WebSite X5Help Center

14 ANSWERS
Daniel W.
Daniel W.
User
Best User of the month DEBest User of the month EN

I don't know if I understood the problem correctly. Files can be uploaded to your own web server or web space using an FTP program or using the FTP window of WebSite X5.

-----

Read more
Posted on the from Daniel W.
Raoul R.
Raoul R.
User
Author

PHP class for your exact feed

Save as:

/includes/klantenvertellen_feed.php

<?php
declare(strict_types=1);

final class KlantenVertellenFeed
{
private string $feedUrl;
private string $cacheFile;
private int $cacheTtlSeconds;

public function __construct(string $feedUrl, string $cacheFile, int $cacheTtlSeconds = 21600)
{
$this->feedUrl = $feedUrl;
$this->cacheFile = $cacheFile;
$this->cacheTtlSeconds = $cacheTtlSeconds;
}

/**
* @return array{
* locationName:string,
* averageRating:string,
* numberReviews:string,
* viewReviewUrl:string,
* reviews:array<int, array{
* author:string,
* city:string,
* rating:string,
* reviewBody:string,
* datePublished:string,
* language:string
* }>
* }
*/
public function getData(int $maxReviews = 3): array
{
$xml = $this->loadXml();

if (!$xml instanceof SimpleXMLElement) {
return [
'locationName' => 'ClubTaxi',
'averageRating' => '',
'numberReviews' => '',
'viewReviewUrl' => 'https://www.klantenvertellen.nl/reviews/1023381/clubtaxi'
'reviews' => [],
];
}

$data = [
'locationName' => trim((string) $xml->locationName),
'averageRating' => trim((string) $xml->averageRating),
'numberReviews' => trim((string) $xml->numberReviews),
'viewReviewUrl' => trim((string) $xml->viewReviewUrl),
'reviews' => [],
];

if (isset($xml->reviews->reviews)) {
$count = 0;

foreach ($xml->reviews->reviews as $reviewNode) {
$reviewBody = $this->extractReviewText($reviewNode);

$data['reviews'][] = [
'author' => trim((string) $reviewNode->reviewAuthor) ?: 'Klant',
'city' => trim((string) $reviewNode->city),
'rating' => trim((string) $reviewNode->rating),
'reviewBody' => $reviewBody,
'datePublished' => $this->normaliseDate((string) $reviewNode->dateSince),
'language' => trim((string) $reviewNode->reviewLanguage),
];

$count++;
if ($count >= $maxReviews) {
break;
}
}
}

return $data;
}

private function extractReviewText(SimpleXMLElement $reviewNode): string
{
if (!isset($reviewNode->reviewContent->reviewContent)) {
return '';
}

$oneLiner = '';
$opinion = '';

foreach ($reviewNode->reviewContent->reviewContent as $contentNode) {
$group = trim((string) $contentNode->questionGroup);
$value = trim((string) $contentNode->rating);

if ($group === 'DEFAULT_ONELINER' && $value !== '') {
$oneLiner = $value;
}

if ($group === 'DEFAULT_OPINION' && $value !== '') {
$opinion = $value;
}
}

// Prefer fuller opinion text; fall back to one-liner.
if ($opinion !== '') {
return $opinion;
}

return $oneLiner;
}

private function loadXml(): ?SimpleXMLElement
{
$xmlString = $this->getXmlString();

if ($xmlString === null || trim($xmlString) === '') {
return null;
}

libxml_use_internal_errors(true);
$xml = simplexml_load_string($xmlString);

return $xml instanceof SimpleXMLElement ? $xml : null;
}

private function getXmlString(): ?string
{
if ($this->isCacheValid()) {
$cached = @file_get_contents($this->cacheFile);
return $cached !== false ? $cached : null;
}

$fresh = $this->downloadFeed();
if ($fresh !== null) {
$this->writeCache($fresh);
return $fresh;
}

if (is_file($this->cacheFile)) {
$cached = @file_get_contents($this->cacheFile);
return $cached !== false ? $cached : null;
}

return null;
}

private function isCacheValid(): bool
{
return is_file($this->cacheFile)
&& filemtime($this->cacheFile) !== false
&& (time() - (int) filemtime($this->cacheFile) < $this->cacheTtlSeconds);
}

private function downloadFeed(): ?string
{
$context = stream_context_create([
'http' => [
'method' => 'GET',
'timeout' => 10,
'header' => implode("\r\n", [
'User-Agent: ClubTaxi Review Feed Reader/1.0',
'Accept: application/xml,text/xml,*/*;q=0.8',
]),
],
'ssl' => [
'verify_peer' => true,
'verify_peer_name' => true,
],
]);

$result = @file_get_contents($this->feedUrl, false, $context);

return $result !== false ? $result : null;
}

private function writeCache(string $xml): void
{
$dir = dirname($this->cacheFile);

if (!is_dir($dir)) {
@mkdir($dir, 0775, true);
}

@file_put_contents($this->cacheFile, $xml, LOCK_EX);
}

private function normaliseDate(string $date): string
{
$date = trim($date);

if ($date === '') {
return '';
}

try {
$dt = new DateTimeImmutable($date);
return $dt->format('Y-m-d');
} catch (Throwable $e) {
return '';
}
}
}

Homepage usage

Place this where you want the review block to appear:

<?php
require_once __DIR__ . '/includes/klantenvertellen_feed.php';

$kvFeed = new KlantenVertellenFeed(
'https://www.klantenvertellen.nl/v1/review/feed.xml?hash=o8st5h6aoduubpq'
__DIR__ . '/cache/klantenvertellen_feed.xml',
21600 // 6 hours
);

$kv = $kvFeed->getData(3);
?>

<section class="reviews-panel">
<h3>Beoordeeld op KlantenVertellen</h3>

<?php if ($kv['averageRating'] !== '' && $kv['numberReviews'] !== ''): ?>
<div class="reviews-score">
<span class="score"><?php echo htmlspecialchars($kv['averageRating'], ENT_QUOTES, 'UTF-8'); ?></span>
<span class="score-meta">/ 10 op basis van <?php echo htmlspecialchars($kv['numberReviews'], ENT_QUOTES, 'UTF-8'); ?> beoordelingen</span>
</div>
<?php endif; ?>

<p class="reviews-intro">
ClubTaxi wordt door klanten beoordeeld om betrouwbaarheid, stiptheid en comfortabele ritten.
</p>

<?php if (!empty($kv['reviews'])): ?>
<div class="review-quotes">
<?php foreach ($kv['reviews'] as $review): ?>
<blockquote class="review-quote">
<p>“<?php echo htmlspecialchars($review['reviewBody'], ENT_QUOTES, 'UTF-8'); ?>”</p>
<footer>
– <?php echo htmlspecialchars($review['author'], ENT_QUOTES, 'UTF-8'); ?>
<?php if ($review['city'] !== ''): ?>
uit <?php echo htmlspecialchars($review['city'], ENT_QUOTES, 'UTF-8'); ?>
<?php endif; ?>
<?php if ($review['rating'] !== ''): ?>
(<?php echo htmlspecialchars($review['rating'], ENT_QUOTES, 'UTF-8'); ?>/10)
<?php endif; ?>
</footer>
</blockquote>
<?php endforeach; ?>
</div>
<?php endif; ?>

<p class="reviews-link">
<a href="<?php echo htmlspecialchars($kv['viewReviewUrl'], ENT_QUOTES, 'UTF-8'); ?>" target="_blank" rel="noopener noreferrer">
Bekijk alle beoordelingen op KlantenVertellen
</a>
</p>
</section>

CSS

.reviews-panel {
max-width: 900px;
margin: 2rem auto;
padding: 1.5rem;
border: 1px solid #ddd;
border-radius: 12px;
background: #fff;
}

.reviews-panel h3 {
margin-top: 0;
margin-bottom: 0.75rem;
}

.reviews-score {
margin-bottom: 1rem;
line-height: 1.2;
}

.reviews-score .score {
font-size: 2.2rem;
font-weight: 700;
}

.reviews-score .score-meta {
display: block;
font-size: 1rem;
}

.reviews-intro {
margin-bottom: 1.25rem;
}

.review-quotes {
display: grid;
gap: 1rem;
}

.review-quote {
margin: 0;
padding: 1rem;
border-left: 4px solid #ccc;
background: #f8f8f8;
border-radius: 6px;
}

.review-quote p {
margin: 0 0 0.5rem 0;
}

.review-quote footer {
font-size: 0.95rem;
}

.reviews-link {
margin-top: 1.25rem;
}

.reviews-link a {
font-weight: 600;
text-decoration: none;
}

.reviews-link a:hover,
.reviews-link a:focus {
text-decoration: underline;
}

Dynamic JSON-LD from the same feed

This lets you keep your rating data automatically up to date.

<?php if ($kv['averageRating'] !== '' && $kv['numberReviews'] !== ''): ?>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "ClubTaxi",
"url": "https://www.clubtaxi.nl",
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "<?php echo htmlspecialchars($kv['averageRating'], ENT_QUOTES, 'UTF-8'); ?>",
"bestRating": "10",
"reviewCount": "<?php echo htmlspecialchars($kv['numberReviews'], ENT_QUOTES, 'UTF-8'); ?>"
}
}
</script>
<?php endif; ?>

There is also a code suggested by ChatGPT for filtering language. But hey, this is way over my head already : ) Thanks

Read more
Posted on the from Raoul R.
Raoul R.
Raoul R.
User
Author

Thanks Daniel, I got that. But how to implement the feed.xml into the project, that it gets uploaded, to a proper folder so that bots know to crawl the feed. That is the thing.

Read more
Posted on the from Raoul R.
Daniel W.
Daniel W.
User
Best User of the month DEBest User of the month EN

If I understood correctly, then the PHP code is probably saved in the file klantenvertellen.php (and not in the file feed.xml) and loaded into the "includes" directory on the web space.

The feed has the file extension .php - for WebSite X5 e.g. x5feed.php (RSS feed).

With WebSite X5, the RSS feed readers need the file x5feed.php and the review feed probably needs the file klantenvertellen.php - if I understood that correctly.

Read more
Posted on the from Daniel W.
Raoul R.
Raoul R.
User
Author

I have not saved anything yet related to the feed.xml. But I looked at using the RSS feed tool as well. And my conclusion was, that I cannot for this purpose. If that is what you are saying, correct: we are on the same page.

Read more
Posted on the from Raoul R.
Raoul R.
Raoul R.
User
Author

The fact that platforms do not allow embedding is a common thing, I do not agree but it is what it is. Hosted by Godaddy means to me that I do not want to experiment, cause they are experts in blaming the customer for things that go wrong. I like not to edit files via their dashboard. I think the conclusion is that the team looks at the above as a request to develope a utility widget for this purpose. Await respons from the team, thanks Daniel, sleep well : )

Read more
Posted on the from Raoul R.
Daniel W.
Daniel W.
User
Best User of the month DEBest User of the month EN

The problem is probably that the PHP script wants to access a third-party website - klantenvertellen.nl - and is therefore blocked by the GoDaddy firewall.

Let's see if the Incomedia staff has a solution or just point you to GoDaddy support.

Maybe GoDaddy users have some tips on how to solve the problem.

I'll say goodnight!

-----

Read more
Posted on the from Daniel W.
Raoul R.
Raoul R.
User
Author

to be clear, I did not upload anything yet, nothing got refused by godaddy. I cannot create an upload that fits the criteria given and described in the above. Thanks Daniel, but that widget is not creating the files that I think I need on the server as well. Await respons from the team.

Read more
Posted on the from Raoul R.
Incomedia
Eric C.
Incomedia

Hello Raoul,
I could not understand the request unfortunately: could you clarify why uploading the file in question manually, as mentioned by Daniel initially, is not a viable option?

Read more
Posted on the from Eric C.
Raoul R.
Raoul R.
User
Author

Because Godaddy gives me an headache if I use their dash, only way to protect myself against those robbers is to be able to say "I did not touch anything

But the question is: Can it be done with your software, or not? And if not, is it something you will be willing to develop?

Read more
Posted on the from Raoul R.
Incomedia
Eric C.
Incomedia

Hello Raoul,
what do you mean by "their dash"?
Files can be manually uploaded through WebSite X5, the process mentioned by Daniel was showing the steps to follow in the software and not external tools.

Read more
Posted on the from Eric C.
Raoul R.
Raoul R.
User
Author

and ofcourse, the real fight is: embedding the page by the platform. And I am fighting, with focus on the target.

Read more
Posted on the from Raoul R.
Raoul R.
Raoul R.
User
Author

I have not tried it, but let's assume I can drop the file, it will be a txt file. Nothing else. As far as I know, there is no file as klantenvertellen.php , there is the css component (which I cannot create), no clue how I can follow your instructions, and come to the desired result. That the data gets crawled, and preferable a selection gets published on the site. Say the 3 or 4 latest in each language. I just do not see it as doable (for me). So it can be the way to do it, but not for me. 

Read more
Posted on the from Raoul R.