Well, I think defining a function is more suitable in your case. Just #let link(dest, body) = .., and there’s almost no difference in usage.
#let link(dest, body) = std.link(dest, {
body
box(image("assets/link icon.jpg", height: 0.8em))
})
// If you omit the body sometimes, `#let link(dest, ..body)` and check if `body.pos()` is empty.
Bonus: You could get rid of the icon easily when appropriate, or use dedicated icons for special links such as mailto: and github in a specifc chapter.
This is also a recursive show rule, but it works. The trick is checking whether the show rule is triggered by itself and just return it in that case. In your example, it would probably need to read if it.body == box(image("assets/link icon.jpg", height: 0.8em)) or something like that.
I think it’s cleaner here to have a single link, so ideally combine this with Y.D.X’s solution. Then the body consists of the original body and the new body; you’ll need something like if it.body.func() == [].func() and it.body.children.last() == ... (body is a sequence ending in the image). When writing this code, it’s super useful to temporarily add repr(it.body) to your show rule to see exactly what you’re trying to match.