Ghost/ghost/link-replacer/test/fixtures/example-post.html
Kevin Ansfield b704530d74
🐛 Fixed unexpected conversion of single-quoted attributes in HTML cards (#19727)
closes ENG-627

We were using `cheerio` to parse+modify+serialize our rendered HTML to modify links for member attribution. Cheerio's serializer has a [long-standing issue](https://github.com/cheeriojs/cheerio/issues/720) (that we've [had to deal with before](https://github.com/TryGhost/SDK/issues/124)) where it replaces single-quote attributes with double-quote attributes. That was resulting in broken rendering when content used single-quotes such as in HTML cards that have JSON data inside a `data-` attribute or otherwise used single-quotes to avoid escaping double-quotes in an attribute value.

- swapped the implementation that uses `cheerio` for one that uses `html5parser` to tokenize the html string, from there we can loop over the tokens and replace the href attribute values in the original string without touching any other part of the content. Avoids a full parse+serialize process which is both more costly and can result unexpected content changes due to serializer opinions.
  - fixes the quote change bug
  - uses tokenization directly to avoid cost of building a full AST
- updated Content API Posts snapshot
  - one of our fixtures has a missing closing tag which we're no longer "fixing" with a full parse+serialize step in the link replacer (keeps modified src closer to original and better matches behaviour elsewhere in the app / without member-attribution applied)
  - the link replacer no longer converts `attr=""` to `attr` (these are equivalent in the HTML spec so no change in behaviour other than preserving the original source html)
- added a benchmark test file comparing the two implementations because the link replacer runs on render so it's used in a hot path
  - new implementation has a 3x performance improvement
  - the separate files with the old/new implementations have been cleaned up but I've left the benchmark test file in place for future reference

Benchmark results comparing implementations:

```
❯ node test/benchmark.js

LinkReplacer
├─ cheerio: 5.03K /s ±2.20%
├─ html5parser: 16.5K /s ±0.43%

Completed benchmark in 0.9976526670455933s
┌─────────────┬─────────┬────────────┬─────────┬───────┐
│   (index)   │ percent │ iterations │ current │  max  │
├─────────────┼─────────┼────────────┼─────────┼───────┤
│   cheerio   │   ''    │ '5.03K/s'  │  5037   │ 5037  │
│ html5parser │   ''    │ '16.5K/s'  │  16534  │ 16534 │
└─────────────┴─────────┴────────────┴─────────┴───────┘
```
2024-03-06 09:11:49 +00:00

79 lines
5.6 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<p>Faster and more robust than ever before, we just shipped a complete rewrite of the Ghost editor. This is our third
major iteration of the Ghost editor, packed with new features, including:</p>
<ul>
<li><a href="https://ghost.org/changelog/image-editor/"><strong>Native image editing</strong></a> - so you can
adjust photos on the fly</li>
<li><a href="https://ghost.org/changelog/post-history/"><strong>Post history</strong></a> - so you can see who
edited what, when, and restore old versions</li>
<li><a href="https://ghost.org/changelog/create-landing-pages/"><strong>Landing page cards</strong></a> - so you can
build beautiful custom experiences</li>
<li><a href="https://ghost.org/changelog/bookmarker/"><strong>Bookmarking</strong></a> - so you can collect links
from around the web for your posts</li>
</ul>
<p>And some fixes for longstanding issues with our previous editor, like:</p>
<ul>
<li><strong>Faster overall performance</strong> - things just feel more <em>snappy</em></li>
<li><strong>Improved handling of very large posts</strong> - which, in the past, was... painful</li>
<li><strong>Better undo/redo chaining</strong> - a smoother experience when fixing mistakes</li>
<li><strong>Much improved mobile editing</strong> - so you can write on the go in iOS / Android</li>
<li><strong>Nested lists</strong> - for structuring your bulleted thoughts<ul>
<li>Which wasn't possible before<ul>
<li>But is now</li>
</ul>
</li>
</ul>
</li>
<li><strong>More keyboard shortcuts</strong> - find the full list in the post settings menu</li>
</ul>
<p>The new editor is now available across all Ghost installs. <a
href="https://ghost.org/pricing/"><strong>Ghost(Pro)</strong></a> users can log into their sites to give it a
try. If you're a developer, self-hosting Ghost, you'll need to <a href="https://ghost.org/docs/update/">update</a>
to the latest version to get access to everything that's new.</p>
<hr>
<h2 id="developer-changes">Developer changes</h2>
<p>Keep reading below if you're curious about the technical details behind the new editor, and what it means if you're
building API integrations with Ghost.</p>
<figure class="kg-card kg-image-card kg-width-wide"><img
src="https://ghost.org/changelog/content/images/2023/10/Frame-1--4-.png" class="kg-image" alt="" loading="lazy"
width="2000" height="1052"></figure>
<p>As we worked on this new editor, one of our main goals was to keep things the same. We made a few visual tweaks here
and there, but for the most part it's still the same editor you know and love... it just works better than it did
before.</p>
<p>Under the hood, though, the technical changes we've made to the editor unlock exciting possibilities for the future.
</p>
<p>Ghost's editor, called Koenig, was previously built in <a href="https://emberjs.com/">Ember.js</a> on an open
JSON-based document storage format called <a href="https://github.com/bustle/mobiledoc-kit">MobileDoc</a>. We loved
how it worked, but MobileDoc never became widely adopted, so the technology underpinning our editor became a bit
stagnant. This limited our ability to build new features, or solve frustrating core bugs (like better mobile
support).</p>
<p>Koenig has now been rebuilt on a new stack: <a href="https://react.dev/">React.js</a> and <a
href="https://lexical.dev/">Lexical</a> — both of which are open source frameworks developed by Meta. So, Ghost
is now using the same underlying technology that powers every single editor, comment box, or user input for billions
of users across Facebook and Instagram.</p>
<figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img
src="https://ghost.org/changelog/content/images/2023/10/Screenshot-2023-10-23-at-16.44.43@2x.png"
class="kg-image" alt="" loading="lazy" width="2000" height="1246">
<figcaption><span style="white-space: pre-wrap;">Try the new Koenig editor for yourself — </span><a
href="https://koenig.ghost.org/"><span style="white-space: pre-wrap;">https://koenig.ghost.org</span></a>
</figcaption>
</figure>
<p>Ghost is the first independent company outside of Meta to build a full-scale dynamic editor on top of Lexical, and we
worked directly with the Lexical core team to make it happen. Today's announcement reflects over a year of quiet,
dedicated work by both teams to get to where we are now.</p>
<p>We have lots of plans for continuing to improve Ghost's editing experience, and this shift in architecture has opened
a lot of new doors for what's possible next.</p>
<p>For developers building integrations with Ghost, check out our updated API docs, which cover how to interact with
Lexical content stored in the database:</p>
<figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container"
href="https://ghost.org/docs/admin-api/#posts">
<div class="kg-bookmark-content">
<div class="kg-bookmark-title">Ghost Admin API Documentation</div>
<div class="kg-bookmark-description">Manage content via Ghosts Admin API, with secure role-based
authentication. Read more on Ghost Docs 👉</div>
<div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://ghost.org/favicon.ico"
alt=""><span class="kg-bookmark-author">Ghost - The Professional Publishing Platform</span></div>
</div>
<div class="kg-bookmark-thumbnail"><img src="https://ghost.org/images/meta/ghost-docs.png" alt=""></div>
</a></figure>
<p></p>