diff --git a/CHANGELOG.md b/CHANGELOG.md
index f5035abc3..37042d237 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## [Unreleased]
+### Changed
+
+- De-duplicate identical `'
+ )
+
+ const Helmet = loadHelmet()
+
+ // This is the hydration case: the component that declared the script
+ // mounts on the client, but the browser already ran the server-rendered
+ // tag, so forwarding it to react-helmet would make it execute twice.
+ render()
+
+ expect(renderedScriptSrcs()).toEqual([])
+})
+
+it('drops a server-rendered script src declared as a JSX child', () => {
+ givenServerRenderedHead(
+ ''
+ )
+
+ const Helmet = loadHelmet()
+
+ render(
+
+
+
+ )
+
+ expect(renderedChildScriptSrcs()).toEqual([])
+})
+
+it('makes react-helmet ignore the adopted tags so they are never removed', () => {
+ givenServerRenderedHead(
+ ''
+ )
+
+ loadHelmet()
+
+ // Without `data-react-helmet`, react-helmet no longer recognizes the tag
+ // as its own, so it won't remove it during its first DOM sync -- the tag
+ // stays exactly where the server put it.
+ expect(document.head.querySelectorAll('script[src]')).toHaveLength(1)
+ expect(
+ document.head.querySelectorAll('script[src][data-react-helmet]')
+ ).toHaveLength(0)
+})
+
+it('only adopts server-rendered scripts that have a src', () => {
+ givenServerRenderedHead(
+ ''
+ )
+
+ const Helmet = loadHelmet()
+
+ render()
+
+ expect(renderedScriptSrcs()).toEqual(['https://example.com/a.js'])
+})
+
+it('does not adopt scripts that react-helmet did not render', () => {
+ givenServerRenderedHead('')
+
+ const Helmet = loadHelmet()
+
+ // A script the page put in the head by itself isn't react-helmet's to
+ // manage, so react-helmet won't remove/re-add it and there's nothing to
+ // de-duplicate against.
+ render()
+
+ expect(renderedScriptSrcs()).toEqual(['https://example.com/a.js'])
+})
+
+it('still forwards scripts that were not server-rendered', () => {
+ givenServerRenderedHead(
+ ''
+ )
+
+ const Helmet = loadHelmet()
+
+ render(
+
+ )
+
+ expect(renderedScriptSrcs()).toEqual(['https://example.com/b.js'])
+})
+
+it('starts each fresh module instance with an empty registry', () => {
+ const FirstHelmet = loadHelmet()
+
+ render()
+ expect(renderedScriptSrcs()).toEqual(['https://example.com/a.js'])
+ cleanup()
+
+ // Sanity check for the test helper itself: a fresh module (as if the
+ // page had reloaded) must not remember the previous instance's src.
+ const SecondHelmet = loadHelmet()
+
+ render()
+ expect(renderedScriptSrcs()).toEqual(['https://example.com/a.js'])
+})
diff --git a/react/components/Helmet.tsx b/react/components/Helmet.tsx
index 94063efb5..6162204b2 100644
--- a/react/components/Helmet.tsx
+++ b/react/components/Helmet.tsx
@@ -1,4 +1,194 @@
-import { Helmet } from 'react-helmet'
+// NOTE: using plain (non-`import type`) imports for the type-only names
+// below (`ReactNode`, `HelmetProps`) is intentional -- the project's
+// installed `babel-preset-react-app` (7.0.2) doesn't support the
+// `import type { ... }` syntax and jest fails to parse it.
+import React, { Children, isValidElement, ReactNode } from 'react'
+import { Helmet as ReactHelmet, HelmetProps } from 'react-helmet'
+
+/**
+ * `