There is nothing overtly incorrect with your code.
However, one thing that makes PHP nice to work with for web development is it's ability to interpolate variables. When you use double quotes around a string, PHP will interpolate (replace variables with their value). When you do not want that behavior, then you should use single quotes. You should also use single quotes to delimit keys in arrays, rather than double quotes, which will also interpolate variables, should they be found.
Here's what I'd do in your case, inside the foreach.
$html .= "<li><h2>{$post['title']}</h2><span>{$post['text']}</span></li>";
This looks more like html, and doesn't require dropping in and out of the string output or concatenating all over the place.
You might wonder why you need the blocks around the $post array variables. This is so that you can include the single quotes around the array key without confusing the php parser. If you were to try this:
$html .= "<li><h2>$post['title']</h2><span>$post['text']</span></li>";
You'll find you get a parse error. The block tells php to parse that code and insure it works fine. You can actually omit the single quotes and the code will parse, but it's better stylistically to always include them when dealing with array keys. This works without the block.
$html .= "<li><h2>$post[title]</h2><span>$post[text]</span></li>";
Whilst this might be a micro-optimization, when you omit the single quotes, php has to determine whether or not 'title' or 'text' are actually constants you've defined. It's also just in general, good practice to always reference variables in the same manner, for consistency and readability.
If you want your html markup to look a bit cleaner, it's also easy enough to add newlines using "\n".
$html = "<ul>\n";
foreach($posts as $post) {
$html .= "<li><h2>{$post['title']}</h2><span>{$post['text']}</span></li>\n";
}
$html .= "</ul>\n";
One final note, since this is a code review -- what this code does, is intermix markup and data in a way that they can not be separated. There is a school of thought in the design pattern world, that data (models) should be pure, and that "views" should be responsible for output. For example, let's say that you want to provide this same data in json format for an ajax call you're doing someplace else in your code. This routine is not going to work, because you baked in the html markup.
One of the hallmarks of a spaghetti/php application is rampant intermixing of logic and HTML code, and views are a great way to avoid that from the get-go.
Your code will work, but it inherently has the weakness of intermixing your logic and data with your markup.
Template systems like smarty or twig to name a few (although most all of the PHP frameworks that provide MVC have a template system) encourage you to put your presentation (view) code into separate files. When it comes time to output your code, you inject the data into the view, and your view code can then create your output. You can do this in a lot of different ways, including using pure old PHP scripts, and alternative PHP syntax. I'd encourage you to look at some examples in the manuals of these template/view libraries.
As a simple example, here's how you could have your code in a php "view" file that you could require or include.
/* You might name this file postView.php */
/* You'll start in HTML mode */
<ul>
<? foreach($posts as $post): ?>
<li><h2><?=$post['title']?></h2><span><?=$post['text']?></span></li>
<? endforeach; ?>
</ul>
Now your logic looks like:
<?php
// Fill your $post from the $_POST super glob.
// Do whatever other logic you need
// Time for output
require_once(postView.php);