WebSite X5Help Center

9 ANTWORTEN
Daniel W.
Daniel W.
User
Nutzer des Monats DENutzer des Monats 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.

-----

Mehr lesen
Gepostet am von Daniel W.
Raoul R.
Raoul R.
User
Autor

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

Mehr lesen
Gepostet am von Raoul R.
Raoul R.
Raoul R.
User
Autor

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.

Mehr lesen
Gepostet am von Raoul R.
Daniel W.
Daniel W.
User
Nutzer des Monats DENutzer des Monats 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.

Mehr lesen
Gepostet am von Daniel W.
Raoul R.
Raoul R.
User
Autor

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.

Mehr lesen
Gepostet am von Raoul R.
Raoul R.
Raoul R.
User
Autor

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 : )

Mehr lesen
Gepostet am von Raoul R.
Daniel W.
Daniel W.
User
Nutzer des Monats DENutzer des Monats 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!

-----

Mehr lesen
Gepostet am von Daniel W.
Raoul R.
Raoul R.
User
Autor

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.

Mehr lesen
Gepostet am von Raoul R.