diff --git a/typo3/sysext/core/Documentation/Changelog/12.1/Deprecation-99040-DeprecatedTypoScriptSetupConstantsTop-level-object.rst b/typo3/sysext/core/Documentation/Changelog/12.1/Deprecation-99040-DeprecatedTypoScriptSetupConstantsTop-level-object.rst
new file mode 100644
index 0000000000000000000000000000000000000000..43dc1a8c84752239a38a6a4c309ff9bdb4f9b543
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/12.1/Deprecation-99040-DeprecatedTypoScriptSetupConstantsTop-level-object.rst
@@ -0,0 +1,92 @@
+.. include:: /Includes.rst.txt
+
+.. _deprecation-99040-1668076207:
+
+==============================================================================
+Deprecation: #99040 - Deprecated TypoScript setup "constants" top-level-object
+==============================================================================
+
+See :issue:`99040`
+
+Description
+===========
+
+The Frontend TypoScript setup (!) top-level-object :typoscript:`constants` can be
+used to define constants for replacement inside a :typoscript:`parseFunc`.
+If :typoscript:`parseFunc` somewhere is configured with :typoscript:`.constants = 1`,
+then all occurrences of the constant in the text will be substituted with the
+actual value.
+
+This construct has been marked as deprecated in TYPO3 v12 and will be removed with v13.
+
+
+Impact
+======
+
+Using the :typoscript:`constants` top-level-object in combination with the
+:typoscript:`constants = 1` in :typoscript:`parseFunc` to substitute strings
+like :typoscript:`###MY_CONSTANT###` triggers a deprecation level log error
+in TYPO3 v12 and will stop working in v13.
+
+
+Affected installations
+======================
+
+This is a relatively rarely used feature, not well-known by many integrators.
+TYPO3 integrators should watch out for :typoscript:`###` markers within
+TypoScript, the Backend Template module search functionality should help here.
+
+The Template Analyzer will also show usages of the setup top-level-object
+:typoscript:`constants`.
+
+
+Migration
+=========
+
+One possible solution is to switch to TypoScript constants / settings instead
+for simple cases.
+
+A simple example usage before:
+
+.. code-block:: typoscript
+
+    TypoScript setup:
+
+    constants.EMAIL = mail@example.com
+    page = PAGE
+    page.10 = TEXT
+    page.10.value = Write an email to ###EMAIL###
+    page.10.parseFunc.constants = 1
+
+Switching to a TypoScript constant / setting:
+
+.. code-block:: typoscript
+
+    TypoScript constants / settings:
+
+    myEmail = mail@example.com
+
+    TypoScript setup:
+
+    page = PAGE
+    page.10 = TEXT
+    page.10.value = Write an email to {$myEmail}
+
+The main usage of this feature has been a "magic" substitution within :typoscript:`lib.parseFunc_RTE`:
+When :sql:`tt_content` rich text content elements contain such substitution strings, they are
+replaced by :typoscript:`parseFunc` accordingly. For instance, a tt_content RTE element with the
+content `Send an email to ###EMAIL###` would substitute to `Send an email to email@example.com` *if*
+the top-level setup :typoscript:`constants` object has been set up. This substitution
+relies on the the fact that editors actively know about and use this construct: If only one content
+element did not prepare for this - since an editor forgot or hasn't been trained about it, changing
+such a constant on TypoScript level would still lead to faulty Frontend output, rendering the
+entire substitution approach useless.
+
+In case instances still rely on this magic substitution principle, and made sure all editors
+always know and follow this approach, instances can use the :typoscript:`userFunc`
+property of :typoscript:`parseFunc` to re-implement the functionality: Basically by
+copying the deprecated code to an own class and registering the :typoscript:`userFunc`
+in :typoscript:`lib.parseFunc_RTE`.
+
+
+.. index:: TypoScript, NotScanned, ext:frontend
diff --git a/typo3/sysext/fluid/Classes/ViewHelpers/Format/HtmlViewHelper.php b/typo3/sysext/fluid/Classes/ViewHelpers/Format/HtmlViewHelper.php
index ba7f393066766b1eebdf778bc3b525882450ba02..30c2d0162a20e6553244b7595825265060c67c63 100644
--- a/typo3/sysext/fluid/Classes/ViewHelpers/Format/HtmlViewHelper.php
+++ b/typo3/sysext/fluid/Classes/ViewHelpers/Format/HtmlViewHelper.php
@@ -42,20 +42,20 @@ use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic;
  *
  * ::
  *
- *    <f:format.html>###PROJECT### is a cool <b>CMS</b> (<a href="https://www.typo3.org">TYPO3</a>).</f:format.html>
+ *    <f:format.html>{$myConstant.project} is a cool <b>CMS</b> (<a href="https://www.typo3.org">TYPO3</a>).</f:format.html>
  *
  * Output::
  *
  *    <p class="bodytext">TYPO3 is a cool <strong>CMS</strong> (<a href="https://www.typo3.org" target="_blank">TYPO3</a>).</p>
  *
- * Depending on TYPO3 setup.
+ * Depending on TYPO3 constants.
  *
  * Custom parseFunc
  * ----------------
  *
  * ::
  *
- *    <f:format.html parseFuncTSPath="lib.parseFunc">###PROJECT### is a cool <b>CMS</b> (<a href="https://www.typo3.org">TYPO3</a>).</f:format.html>
+ *    <f:format.html parseFuncTSPath="lib.parseFunc">TYPO3 is a cool <b>CMS</b> (<a href="https://www.typo3.org">TYPO3</a>).</f:format.html>
  *
  * Output::
  *
diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Format/HtmlViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Format/HtmlViewHelperTest.php
index 5ea83ab0b16a4c3cbda691aec522207b9167ba34..b3119b0cb095a69243cccd9df6dd488c27ff97e2 100644
--- a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Format/HtmlViewHelperTest.php
+++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Format/HtmlViewHelperTest.php
@@ -37,7 +37,7 @@ class HtmlViewHelperTest extends FunctionalTestCase
     {
         return [
             'format.html: process lib.parseFunc_RTE by default' => [
-                '<f:format.html>###PROJECT### is a cool CMS</f:format.html>',
+                '<f:format.html>{$project} is a cool CMS</f:format.html>',
                 'TYPO3 is a cool CMS',
             ],
             'format.html: process inline notation with undefined variable returns empty string' => [
@@ -45,7 +45,7 @@ class HtmlViewHelperTest extends FunctionalTestCase
                 '',
             ],
             'format.html: process specific TS path' => [
-                '<f:format.html parseFuncTSPath="lib.foo">###FOO### is BAR</f:format.html>',
+                '<f:format.html parseFuncTSPath="lib.foo">{$foo} is BAR</f:format.html>',
                 'BAR is BAR',
             ],
             'format.html: specific TS path with current' => [
@@ -61,8 +61,8 @@ class HtmlViewHelperTest extends FunctionalTestCase
                 'Hello Bar',
             ],
             'format.html: specific TS path with data, currentValueKey and a constant' => [
-                '<f:format.html parseFuncTSPath="lib.news" data="{uid: 1, pid: 12, title: \'Greate news\'}" currentValueKey="title">###PROJECT### news:</f:format.html>',
-                'TYPO3 news: Greate news',
+                '<f:format.html parseFuncTSPath="lib.news" data="{uid: 1, pid: 12, title: \'Great news\'}" currentValueKey="title">{$project} news:</f:format.html>',
+                'TYPO3 news: Great news',
             ],
             // table attribute is hard to test. It was only used as parent for CONTENT and RECORD cObj.
             // Further the table will be used in FILES cObj as fallback, if a table was not given in references array.
@@ -135,16 +135,16 @@ class HtmlViewHelperTest extends FunctionalTestCase
                     'pid' => 1,
                     'root' => 1,
                     'clear' => 3,
+                    'constants' => <<<EOT
+project = TYPO3
+foo = BAR
+EOT,
                     'config' => <<<EOT
-constants.PROJECT = TYPO3
-constants.FOO = BAR
 lib.parseFunc_RTE {
     htmlSanitize = 1
-    constants = 1
 }
 lib.foo {
     htmlSanitize = 1
-    constants = 1
 }
 lib.inventor {
     htmlSanitize = 1
@@ -158,7 +158,6 @@ lib.record {
 }
 lib.news {
     htmlSanitize = 1
-    constants = 1
     plainTextStdWrap.noTrimWrap = || |
     plainTextStdWrap.dataWrap = |{CURRENT:1}
 }
diff --git a/typo3/sysext/fluid_styled_content/Configuration/TypoScript/Helper/ParseFunc.typoscript b/typo3/sysext/fluid_styled_content/Configuration/TypoScript/Helper/ParseFunc.typoscript
index b06e8cebf1775cc3ccb3b3f001396ff58c802155..548e5dd28450df465463e262b6485bf9c9700b59 100644
--- a/typo3/sysext/fluid_styled_content/Configuration/TypoScript/Helper/ParseFunc.typoscript
+++ b/typo3/sysext/fluid_styled_content/Configuration/TypoScript/Helper/ParseFunc.typoscript
@@ -32,6 +32,7 @@ lib.parseFunc {
     }
     allowTags = {$styles.content.allowTags}
     denyTags = *
+    # @deprecated since TYPO3 v12, remove with v13
     constants = 1
     nonTypoTagStdWrap {
         HTMLparser = 1
diff --git a/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php b/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php
index aa1f46eac01778eee308b5ff9ae2550064af7801..6d1532c163d7701b54a86832339f86e5d275ee46 100644
--- a/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php
+++ b/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php
@@ -2585,11 +2585,8 @@ class ContentObjectRenderer implements LoggerAwareInterface
      * Implements the "if" function in TYPO3 TypoScript
      *
      * @param array $conf TypoScript properties defining what to compare
-     * @return bool
-     * @see stdWrap()
-     * @see _parseFunc()
      */
-    public function checkIf($conf)
+    public function checkIf($conf): bool
     {
         if (!is_array($conf)) {
             return true;
@@ -3081,15 +3078,14 @@ class ContentObjectRenderer implements LoggerAwareInterface
      * This situation has not become better by having a RTE around...
      *
      * This function is actually just splitting the input content according to the configuration of "external blocks".
-     * This means that before the input string is actually "parsed" it will be splitted into the parts configured to BE parsed
+     * This means that before the input string is actually "parsed" it will be split into the parts configured to BE parsed
      * (while other parts/blocks should NOT be parsed).
-     * Therefore the actual processing of the parseFunc properties goes on in ->_parseFunc()
+     * Therefore, the actual processing of the parseFunc properties goes on in ->parseFuncInternal()
      *
      * @param string $theValue The value to process.
      * @param non-empty-array<string, mixed>|null $conf TypoScript configuration for parseFunc
      * @param non-empty-string|null $ref Reference to get configuration from. Eg. "< lib.parseFunc" which means that the configuration of the object path "lib.parseFunc" will be retrieved and MERGED with what is in $conf!
      * @return string The processed value
-     * @see _parseFunc()
      */
     public function parseFunc($theValue, ?array $conf, ?string $ref = null)
     {
@@ -3110,7 +3106,7 @@ class ContentObjectRenderer implements LoggerAwareInterface
         $conf['htmlSanitize'] = (bool)($conf['htmlSanitize'] ?? true);
         // Process:
         if ((string)($conf['externalBlocks'] ?? '') === '') {
-            $result = $this->_parseFunc($theValue, $conf);
+            $result = $this->parseFuncInternal($theValue, $conf);
             if ($conf['htmlSanitize']) {
                 $result = $this->stdWrap_htmlSanitize($result, $conf['htmlSanitize.'] ?? []);
             }
@@ -3199,7 +3195,7 @@ class ContentObjectRenderer implements LoggerAwareInterface
                     $parts[$k] = $this->stdWrap($parts[$k], $cfg['stdWrap.']);
                 }
             } else {
-                $parts[$k] = $this->_parseFunc($parts[$k], $conf);
+                $parts[$k] = $this->parseFuncInternal($parts[$k], $conf);
             }
         }
         $result = implode('', $parts);
@@ -3216,9 +3212,8 @@ class ContentObjectRenderer implements LoggerAwareInterface
      * @param array $conf TypoScript configuration for parseFunc
      * @return string The processed value
      * @internal
-     * @see parseFunc()
      */
-    public function _parseFunc($theValue, $conf)
+    protected function parseFuncInternal($theValue, $conf)
     {
         if (!empty($conf['if.']) && !$this->checkIf($conf['if.'])) {
             return $theValue;
@@ -3274,6 +3269,13 @@ class ContentObjectRenderer implements LoggerAwareInterface
                         }
                         $tmpConstants = $typoScriptSetupArray['constants.'] ?? null;
                         if (!empty($conf['constants']) && is_array($tmpConstants)) {
+                            // @deprecated since v12, remove with v13: Entire if plus init code above
+                            trigger_error(
+                                'The TypoScript setup "constants" top-level-object and the parseFunc property "constants" have'
+                                . ' been deprecated in TYPO3 v12 and will be removed in v12. Use TypoScript constants / settings'
+                                . ' and access them in setup using "{$myConstant}" instead.',
+                                E_USER_DEPRECATED
+                            );
                             foreach ($tmpConstants as $key => $val) {
                                 if (is_string($val)) {
                                     $data = str_replace('###' . $key . '###', $val, $data);
@@ -3305,7 +3307,7 @@ class ContentObjectRenderer implements LoggerAwareInterface
                         foreach ($conf['tags.'] as $tag => $tagConfig) {
                             // only match tag `a` in `<a href"...">` but not in `<abbr>`
                             if (preg_match('#<' . $tag . '[\s/>]#', $data)) {
-                                $data = $this->_parseFunc($data, $conf);
+                                $data = $this->parseFuncInternal($data, $conf);
                                 break;
                             }
                         }
@@ -3553,12 +3555,14 @@ class ContentObjectRenderer implements LoggerAwareInterface
      * Will find all strings prefixed with "http://" and "https://" in the $data string and make them into a link,
      * linking to the URL we should have found.
      *
+     * Helper method of parseFuncInternal().
+     *
      * @param string $data The string in which to search for "http://
      * @param array $conf Configuration for makeLinks, see link
      * @return string The processed input string, being returned.
-     * @see _parseFunc()
+     * @internal
      */
-    public function http_makelinks($data, $conf)
+    protected function http_makelinks(string $data, array $conf): string
     {
         $parts = [];
         foreach (['http://', 'https://'] as $scheme) {
@@ -3607,19 +3611,21 @@ class ContentObjectRenderer implements LoggerAwareInterface
      * Will find all strings prefixed with "mailto:" in the $data string and make them into a link,
      * linking to the email address they point to.
      *
+     * Helper method of parseFuncInternal().
+     *
      * @param string $data The string in which to search for "mailto:
      * @param array $conf Configuration for makeLinks, see link
      * @return string The processed input string, being returned.
-     * @see _parseFunc()
+     * @internal
      */
-    public function mailto_makelinks($data, $conf)
+    protected function mailto_makelinks(string $data, array $conf): string
     {
         $conf = (array)$conf;
         $parts = [];
         // split by mailto logic
         $textpieces = explode('mailto:', $data);
         $pieces = count($textpieces);
-        $textstr = $textpieces[0];
+        $textstr = $textpieces[0] ?? '';
         for ($i = 1; $i < $pieces; $i++) {
             $len = strcspn($textpieces[$i], chr(32) . "\t" . CRLF);
             if (trim(substr($textstr, -1)) === '' && $len) {
@@ -4607,9 +4613,6 @@ class ContentObjectRenderer implements LoggerAwareInterface
      * @param array $conf The TypoScript configuration to pass the function
      * @param mixed $content The content payload to pass the function
      * @return mixed The return content from the function call. Should probably be a string.
-     * @see stdWrap()
-     * @see typoLink()
-     * @see _parseFunc()
      */
     public function callUserFunction($funcName, $conf, $content)
     {
@@ -5676,10 +5679,9 @@ class ContentObjectRenderer implements LoggerAwareInterface
     /**
      * Get content length of the current tag that could also contain nested tag contents
      *
-     * @param string $theValue
-     * @param int $pointer
-     * @param string $currentTag
-     * @return int
+     * Helper method of parseFuncInternal().
+     *
+     * @internal
      */
     protected function getContentLengthOfCurrentTag(string $theValue, int $pointer, string $currentTag): int
     {
diff --git a/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php b/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php
index a556f77dc7df763fcc64e479b49d292dbe109152..e48a0be9c5edcc67ccff66794686622586470231 100644
--- a/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php
+++ b/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php
@@ -2324,7 +2324,7 @@ class ContentObjectRendererTest extends UnitTestCase
         $linkFactory->method('create')->willReturn($linkResult);
         GeneralUtility::addInstance(LinkFactory::class, $linkFactory);
 
-        self::assertSame($expectedResult, $this->subject->http_makelinks($data, $configuration));
+        self::assertSame($expectedResult, $this->subject->_call('http_makelinks', $data, $configuration));
     }
 
     public function invalidHttpMakelinksDataProvider(): array
@@ -2357,7 +2357,7 @@ class ContentObjectRendererTest extends UnitTestCase
      */
     public function httpMakelinksReturnsNoLink(string $data, array $configuration, string $expectedResult): void
     {
-        self::assertSame($expectedResult, $this->subject->http_makelinks($data, $configuration));
+        self::assertSame($expectedResult, $this->subject->_call('http_makelinks', $data, $configuration));
     }
 
     public function mailtoMakelinksDataProvider(): array
@@ -2436,7 +2436,7 @@ class ContentObjectRendererTest extends UnitTestCase
         $linkFactory->method('create')->willReturn($linkResult);
         GeneralUtility::addInstance(LinkFactory::class, $linkFactory);
 
-        self::assertSame($expectedResult, $this->subject->mailto_makelinks($data, $configuration));
+        self::assertSame($expectedResult, $this->subject->_call('mailto_makelinks', $data, $configuration));
     }
 
     /**
@@ -2444,7 +2444,7 @@ class ContentObjectRendererTest extends UnitTestCase
      */
     public function mailtoMakelinksReturnsNoMailToLink(): void
     {
-        self::assertSame('mailto:', $this->subject->mailto_makelinks('mailto:', []));
+        self::assertSame('mailto:', $this->subject->_call('mailto_makelinks', 'mailto:', []));
     }
 
     /**
@@ -5520,7 +5520,7 @@ class ContentObjectRendererTest extends UnitTestCase
                 $content,
                 [],
                 0,
-                null,
+                false,
             ],
             'if. is empty array' => [
                 $content,
@@ -5528,7 +5528,7 @@ class ContentObjectRendererTest extends UnitTestCase
                 $content,
                 ['if.' => []],
                 0,
-                null,
+                false,
             ],
             'if. is null' => [
                 $content,
@@ -5536,7 +5536,7 @@ class ContentObjectRendererTest extends UnitTestCase
                 $content,
                 ['if.' => null],
                 0,
-                null,
+                false,
             ],
             'if. is false' => [
                 $content,
@@ -5544,7 +5544,7 @@ class ContentObjectRendererTest extends UnitTestCase
                 $content,
                 ['if.' => false],
                 0,
-                null,
+                false,
             ],
             'if. is 0' => [
                 $content,
@@ -5552,7 +5552,7 @@ class ContentObjectRendererTest extends UnitTestCase
                 $content,
                 ['if.' => false],
                 0,
-                null,
+                false,
             ],
             'if. is "0"' => [
                 $content,
@@ -5560,7 +5560,7 @@ class ContentObjectRendererTest extends UnitTestCase
                 $content,
                 ['if.' => '0'],
                 0,
-                null,
+                false,
             ],
             'checkIf returning true' => [
                 $content,
@@ -5600,9 +5600,9 @@ class ContentObjectRendererTest extends UnitTestCase
      * @param string $content The given content.
      * @param array $conf
      * @param int $times Times checkIf is called (0 or 1).
-     * @param bool|null $will Return of checkIf (null if not called).
+     * @param bool $will Return of checkIf (null if not called).
      */
-    public function stdWrap_if(string $expect, bool $stop, string $content, array $conf, int $times, ?bool $will): void
+    public function stdWrap_if(string $expect, bool $stop, string $content, array $conf, int $times, bool $will): void
     {
         $subject = $this->getAccessibleMock(
             ContentObjectRenderer::class,