Using the built in summary feature when using ONLY paragraphs module for text I want to have more flexibility in creating content as usual. Therefore, I use the Paragraphs module, and I also disabled the standard body text. So now, a user can now freely decide how he wants to mix images, text and other content (like embedded videos). For example, an article could now start with a text followed by a full size image, or with 2 half size images followed by a full size video and then the text and so on.

So far so good. Now the problem is that I loose the standard summary function since the default body text is just disabled. This is especially a problem for any list of articles, i.e. the front page.

I have managed to kinda solve this problem by a custom preprocess hook:

function MY_THEME_preprocess_node(array &amp;$variables) {

  $node  = \Drupal\node\Entity\Node::load($variables['node']-&gt;id());

  if ($node-&gt;field_paragraph &amp;&amp; !empty($node-&gt;field_paragraph-&gt;getValue())) {

    foreach ($node-&gt;field_paragraph-&gt;getValue() as $key =&gt; $value) {
      $paragraph = FALSE;
      if (!empty($value['entity'])) {
        // Use that, we're probably previewing.
        $paragraph = $value['entity'];
      } elseif (!empty($value['target_id'])) {
        $paragraph = Paragraph::load($value['target_id']);
      }

      if (!empty($paragraph) &amp;&amp; $paragraph instanceof Paragraph) {

        $summary = $paragraph-&gt;getSummary();

        if ($summary &amp;&amp; $paragraph-&gt;type-&gt;getValue()[0]["target_id"] === "text") {
          $variables['summary'] = $summary;
          break;
        }
      }
    }
  }
}


What this does is to loop over all paragraph entries (if any) in a given node and stop iterating at the first occurence of a paragraph type named "text", if there is also a result for the getSummary() function, i.e. this paragraph item also contains some text. Keep in mind that I am far from being a Drupal expert, so this code might not be the optimal way?

However, I cannot seem to control the length of the summary, also any headlines defined in the same paragraph are included as well. Last but not least, the result also contains unneeded markup, so in my node--article--teaser.html.twig twig template I have to strip out those tags again:

&lt;p&gt;{{ summary|striptags }}&lt;/p&gt;


The next problem arises when I want to use that summary for the description meta tags on any given article page (so in this case, NOT the pages that contain only lists of articles). Usually I would just use the Metatags module and use this for the description: [node:summary]

Of course that does not work here, and I can imagine that further down the road I will encounter more problems like this.

Therefore, it seems to me that the cleanest way would be to kinda emulate [node:summary], i.e. that this value is automatically calculated by something similar to the function I used above. So for the consumer of this value this would look identical as if the page / node had an actual "default" summary.

Now, is this possible at all, and if "yes" how can I achieve this? Any insights are greatly appreciated!
Well, looks like it is time again to answer my own question!

So, since the Metatag Module I want to also use is based on tokens anyway, I have managed to implement this using my own module which in turn just defines an additional custom token which in turn returns my desired summary. Here is what my current custom_summary_token.module code looks like right now (still work in progress):

use Drupal\Core\Render\BubbleableMetadata;
use Drupal\paragraphs\Entity\Paragraph;

function custom_summary_token_token_info(){
    $type = [
        'name' =&gt; t('Nodes'),
        'description' =&gt; t('Tokens related to individual nodes.'),
        'needs-data' =&gt; 'node'
    ];

    $node['custom_summary'] = array(
        'name' =&gt; t("Custom Summary"),
        'description' =&gt; t("Token for dynamically generating a node summary, depending on the content. Used for extracting text when not using standard body field, but only paragraphs."),
    );

    return [
        'types' =&gt; ['node' =&gt; $type],
        'tokens' =&gt; ['node' =&gt; $node],
    ];
}

function custom_summary_token_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata){

    if (!isset($data['node'])) { return; }

    $replacements = array();
    $node = $data['node'];

    if($type == 'node' &amp;&amp; !empty($node)) {

        foreach ($tokens as $name =&gt; $original) {

            switch ($name) {

                case 'custom_summary':

                    if (!empty($node-&gt;body-&gt;value)) {

                        $text = strip_tags($node-&gt;body-&gt;value);

                    } else if ($node-&gt;field_summary &amp;&amp; !empty($node-&gt;field_summary-&gt;getValue())) {

                        $text = $node-&gt;field_summary-&gt;getValue()[0]["value"];

                    } else if ($node-&gt;field_paragraph_test &amp;&amp; !empty($node-&gt;field_paragraph_test-&gt;getValue())) {

                        foreach ($node-&gt;field_paragraph_test-&gt;getValue() as $key =&gt; $value) {
                            $paragraph = FALSE;
                            if (!empty($value['entity'])) {
                                $paragraph = $value['entity']; // Use that, we're probably previewing.
                            } elseif (!empty($value['target_id'])) {
                                $paragraph = Paragraph::load($value['target_id']);
                            }
                            if ($paragraph !== FALSE &amp;&amp; $paragraph instanceof Paragraph &amp;&amp; $paragraph-&gt;getType() === "text") {
                                $text = strip_tags($paragraph-&gt;get('field_text_body')-&gt;value);
                                break;
                            }
                        }

                    }

                    if (strlen($text) &gt; 150) {
                        $text = substr($text, 0, 150)." ...";
                    }

                    $text = str_replace("&amp;nbsp;", " ", $text); // filter out artefact when editing with chrome
                    $replacements[$original] = $text;
                    break;
            }
        }
    }

    return $replacements;

}


So what this does in that tokens hook is (after checking that we have the right token of course) first to look for an ordinary default body text which is not empty. Simply because then this also works for my other (in this case also simpler) content types. Then, the code looks for a custom field named field_summary. Turned out that some users actually want to create a custom summary, so yeah now they can do that as well. For those who don't (the majority ...), the code then looks for the first paragraph instance which is also a text block (and not empty). Last but not least, tags are stripped, and the whole thing is truncated to 150 chars, with some "..." appended for good measure.

Now, I can use this just like any token in my Metatag setup: [node:custom_summary], and - with a little help from the twig tweak module - also in any template: 

{{ drupal_token('node:custom_summary', { node: node }) }}


Of course this implementation still might be too naive or there would be an even better way, also I am not sure how big the performance impact of such a solution is. But hey - at least this works!

What would you think?