Skip to content

Commit

Permalink
Embed banners support
Browse files Browse the repository at this point in the history
- added integration of the new AMP API field `mode`
- added possibility to render embed banners
- added new rendering mode `embed` (`EmbedRenderingMode`) for the Latte bridge
- updated docs
- updated CHANGELOG
- fixed and added tests
  • Loading branch information
tg666 committed Dec 12, 2023
1 parent ddbf626 commit 7b048ea
Show file tree
Hide file tree
Showing 49 changed files with 732 additions and 72 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased
- Added integration of the new AMP API field `mode`.
- Added possibility to render embed banners.
- Added new rendering mode `embed` (`EmbedRenderingMode`) for the Latte bridge.

### Changed
- Updated docs

## 1.1.0 - 2023-12-01
### Added
Expand Down
1 change: 1 addition & 0 deletions docs/integration-with-nette-framework.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ The following rendering modes are available:

- **direct** ([DirectRenderingMode](../src/Bridge/Latte/RenderingMode/DirectRenderingMode.php)) - The default mode, API is requested separately for each banner.
- **client_side** ([ClientSideRenderingMode](../src/Bridge/Latte/RenderingMode/ClientSideRenderingMode.php)) - Renders only a wrapper element and leaves loading banners on the JavaScript client. Banners are loaded by calling the `attachBanners()` function.
- **embed** ([EmbedRenderingMode](../src/Bridge/Latte/RenderingMode/EmbedRenderingMode.php)) - Same behavior as `client_side`, but passes the information to the JavaScript client that the banner should be rendered as `embed`.
- **queued_in_presenter_context** ([QueuedRenderingInPresenterContextMode](../src/Bridge/Latte/RenderingMode/QueuedRenderingInPresenterContextMode.php)) - Renders only HTML comments as placeholders and stores requested positions in a queue. It will request, render and place all banners to them positions at once before the presenter returns a response.
- **queued** ([QueuedRenderingMode](../src/Bridge/Latte/RenderingMode/QueuedRenderingMode.php)) - Same behavior as `queued_in_presenter_context`, but it doesn't take into account whether the website template is currently being rendered through the Nette application. It is more suited for an integration without a framework.

Expand Down
50 changes: 50 additions & 0 deletions docs/integration-without-framework.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* [Fetching banners](#fetching-banners)
* [Rendering banners](#rendering-banners)
* [Rendering banners on the client side](#rendering-banners-on-the-client-side)
* [Rendering embed (iframe) banners](#rendering-embed-iframe-banners)
* [Lazy loading of image banners](#lazy-loading-of-image-banners)
* [Templates overwriting](#templates-overwriting)
* [Rendering banners using Latte](#rendering-banners-using-latte)
Expand Down Expand Up @@ -192,6 +193,54 @@ echo $renderer->renderClientSide(new Position('homepage.promo'), ['class' => 'my

Banners rendered in this way will be loaded by the JavaScript client when its `attachBanners()` function is called.

### Rendering embed (iframe) banners

Banners can be rendered in "embed" mode, which means they are inside the `<iframe>` tag.
This rendering mode is again controlled by the JavaScript client.

```php
use SixtyEightPublishers\AmpClient\Renderer\Renderer;
use SixtyEightPublishers\AmpClient\Request\ValueObject\Position;
use SixtyEightPublishers\AmpClient\Renderer\ClientSideMode;

$renderer = Renderer::create();

echo $renderer->renderClientSide(
position: new Position('homepage.top'),
mode: ClientSideMode::embed(),
);
```

The information that the banner should be rendered in the `<iframe>` tag can also be returned by AMP.
If we want to follow this behavior, we need to condition the rendering ourselves.

```php
use SixtyEightPublishers\AmpClient\AmpClientInterface;
use SixtyEightPublishers\AmpClient\Renderer\RendererInterface;
use SixtyEightPublishers\AmpClient\Request\BannersRequest;
use SixtyEightPublishers\AmpClient\Request\ValueObject\Position;
use SixtyEightPublishers\AmpClient\Renderer\ClientSideMode;

/** @var AmpClientInterface $client */
/** @var RendererInterface $renderer */

$request = new BannersRequest([
new Position('homepage.top'),
]);

$response = $client->fetchBanners($request);
$position = $response->getPosition('homepage.top');

if ($position::ModeEmbed === $position->getMode()) {
echo $renderer->renderClientSide(
position: $request->getPosition('homepage.top'),
mode: ClientSideMode::embed(),
);
} else {
echo $renderer->render($position);
}
```

### Lazy loading of image banners

The default client templates support [native lazy loading](https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading#images_and_iframes) of images.
Expand Down Expand Up @@ -367,6 +416,7 @@ The following rendering modes are available:

- **direct** ([DirectRenderingMode](../src/Bridge/Latte/RenderingMode/DirectRenderingMode.php)) - The default mode, API is requested separately for each banner.
- **client_side** ([ClientSideRenderingMode](../src/Bridge/Latte/RenderingMode/ClientSideRenderingMode.php)) - Renders only a wrapper element and leaves loading banners on the JavaScript client. Banners are loaded by calling the `attachBanners()` function.
- **embed** ([EmbedRenderingMode](../src/Bridge/Latte/RenderingMode/EmbedRenderingMode.php)) - Same behavior as `client_side`, but passes the information to the JavaScript client that the banner should be rendered as `embed`.
- **queued** ([QueuedRenderingMode](../src/Bridge/Latte/RenderingMode/QueuedRenderingMode.php)) - Renders only HTML comments as placeholders and stores requested positions in a queue. It will request and render all banners at once when the `RendererProvider::renderQueuedPositions()` method is called.
- **queued_in_presenter_context** ([QueuedRenderingInPresenterContextMode](../src/Bridge/Latte/RenderingMode/QueuedRenderingInPresenterContextMode.php)) - Same behavior as `queued` but in the context of a Presenter only. Usable with Nette applications only.

Expand Down
38 changes: 32 additions & 6 deletions src/Bridge/Latte/RendererProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
use SixtyEightPublishers\AmpClient\Bridge\Latte\Event\ConfigureClientEvent;
use SixtyEightPublishers\AmpClient\Bridge\Latte\Event\ConfigureClientEventHandlerInterface;
use SixtyEightPublishers\AmpClient\Bridge\Latte\RenderingMode\DirectRenderingMode;
use SixtyEightPublishers\AmpClient\Bridge\Latte\RenderingMode\EmbedRenderingMode;
use SixtyEightPublishers\AmpClient\Bridge\Latte\RenderingMode\RenderingModeInterface;
use SixtyEightPublishers\AmpClient\Exception\AmpExceptionInterface;
use SixtyEightPublishers\AmpClient\Exception\RendererException;
use SixtyEightPublishers\AmpClient\Renderer\ClientSideMode;
use SixtyEightPublishers\AmpClient\Renderer\RendererInterface;
use SixtyEightPublishers\AmpClient\Request\BannersRequest;
use SixtyEightPublishers\AmpClient\Request\ValueObject\BannerResource;
Expand Down Expand Up @@ -88,7 +90,11 @@ public function __invoke(object $globals, string $positionCode, array $options =
$position = $this->createPosition($positionCode, $options);

if ($renderingMode->shouldBePositionRenderedClientSide($position)) {
return $this->renderClientSidePosition($position, $options);
return $this->renderClientSidePosition(
$position,
$options,
$renderingMode instanceof EmbedRenderingMode ? ClientSideMode::embed() : ClientSideMode::managed(),
);
}

if ($renderingMode->shouldBePositionQueued($position, $globals)) {
Expand All @@ -101,7 +107,17 @@ public function __invoke(object $globals, string $positionCode, array $options =
return '';
}

return $this->renderPosition($response->getPosition($positionCode), $options);
$responsePosition = $response->getPosition($positionCode);

if ($responsePosition::ModeEmbed === $responsePosition->getMode()) {
return $this->renderClientSidePosition(
$position,
$options,
ClientSideMode::embed(),
);
}

return $this->renderPosition($responsePosition, $options);
}

public function setDebugMode(bool $debugMode): self
Expand Down Expand Up @@ -202,13 +218,23 @@ public function renderQueuedPositions($output)
$replacements = array_filter(
array_map(
function (array $item) use ($response): ?string {
$responsePosition = $response->getPosition($item[0]->getCode());
$requestPosition = $item[0];
$options = $item[1];
$responsePosition = $response->getPosition($requestPosition->getCode());

if (null === $responsePosition) {
return null;
}

return $this->renderPosition($responsePosition, $item[1]);
if ($responsePosition::ModeEmbed === $responsePosition->getMode()) {
return $this->renderClientSidePosition(
$requestPosition,
$options,
ClientSideMode::embed(),
);
}

return $this->renderPosition($responsePosition, $options);
},
$this->queue,
),
Expand Down Expand Up @@ -301,13 +327,13 @@ private function renderPosition(ResponsePosition $position, array $options): str
/**
* @param array<string, mixed> $options
*/
private function renderClientSidePosition(RequestPosition $position, array $options): string
private function renderClientSidePosition(RequestPosition $position, array $options, ClientSideMode $mode): string
{
try {
$elementAttributes = (array) ($options[self::OptionAttributes] ?? []);
$bannerOptions = (array) ($options[self::OptionOptions] ?? []);

return $this->renderer->renderClientSide($position, $elementAttributes, $bannerOptions);
return $this->renderer->renderClientSide($position, $elementAttributes, $bannerOptions, $mode);
} catch (RendererException $e) {
if ($this->debugMode) {
throw $e;
Expand Down
32 changes: 32 additions & 0 deletions src/Bridge/Latte/RenderingMode/EmbedRenderingMode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace SixtyEightPublishers\AmpClient\Bridge\Latte\RenderingMode;

use SixtyEightPublishers\AmpClient\Request\ValueObject\Position;

final class EmbedRenderingMode implements RenderingModeInterface
{
public const Name = 'embed';

public function getName(): string
{
return self::Name;
}

public function supportsQueues(): bool
{
return false;
}

public function shouldBePositionQueued(Position $position, object $globals): bool
{
return false;
}

public function shouldBePositionRenderedClientSide(Position $position): bool
{
return true;
}
}
2 changes: 2 additions & 0 deletions src/Bridge/Nette/DI/AmpClientLatteExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use SixtyEightPublishers\AmpClient\Bridge\Latte\RendererProvider;
use SixtyEightPublishers\AmpClient\Bridge\Latte\RenderingMode\ClientSideRenderingMode;
use SixtyEightPublishers\AmpClient\Bridge\Latte\RenderingMode\DirectRenderingMode;
use SixtyEightPublishers\AmpClient\Bridge\Latte\RenderingMode\EmbedRenderingMode;
use SixtyEightPublishers\AmpClient\Bridge\Latte\RenderingMode\QueuedRenderingInPresenterContextMode;
use SixtyEightPublishers\AmpClient\Bridge\Latte\RenderingMode\QueuedRenderingMode;
use SixtyEightPublishers\AmpClient\Bridge\Nette\Application\AttachPresenterHandlersOnApplicationHandler;
Expand All @@ -38,6 +39,7 @@ final class AmpClientLatteExtension extends CompilerExtension
QueuedRenderingMode::Name => QueuedRenderingMode::class,
QueuedRenderingInPresenterContextMode::Name => QueuedRenderingInPresenterContextMode::class,
ClientSideRenderingMode::Name => ClientSideRenderingMode::class,
EmbedRenderingMode::Name => EmbedRenderingMode::class,
];

private bool $debugMode;
Expand Down
64 changes: 64 additions & 0 deletions src/Renderer/ClientSideMode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

namespace SixtyEightPublishers\AmpClient\Renderer;

use InvalidArgumentException;
use function in_array;
use function sprintf;

final class ClientSideMode
{
public const Modes = [
self::Managed,
self::Embed,
];

private const Managed = 'managed';
private const Embed = 'embed';

private string $value;

private function __construct(string $value)
{
$this->value = $value;
}

public static function managed(): self
{
return new self(self::Managed);
}

public static function embed(): self
{
return new self(self::Embed);
}

public static function fromValue(string $value): self
{
if (!in_array($value, self::Modes, true)) {
throw new InvalidArgumentException(sprintf(
'Value "%s" is not valid client side mode.',
$value,
));
}

return new self($value);
}

public function getValue(): string
{
return $this->value;
}

public function isManaged(): bool
{
return self::Managed === $this->value;
}

public function isEmbed(): bool
{
return self::Embed === $this->value;
}
}
5 changes: 3 additions & 2 deletions src/Renderer/Latte/LatteRendererBridge.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace SixtyEightPublishers\AmpClient\Renderer\Latte;

use Latte\Engine;
use SixtyEightPublishers\AmpClient\Renderer\ClientSideMode;
use SixtyEightPublishers\AmpClient\Renderer\Latte\Templates\ClientSideTemplate;
use SixtyEightPublishers\AmpClient\Renderer\Latte\Templates\MultipleTemplate;
use SixtyEightPublishers\AmpClient\Renderer\Latte\Templates\NotFoundTemplate;
Expand Down Expand Up @@ -83,11 +84,11 @@ public function renderMultiple(ResponsePosition $position, array $banners, array
);
}

public function renderClientSide(RequestPosition $position, array $elementAttributes = [], array $options = []): string
public function renderClientSide(RequestPosition $position, ClientSideMode $mode, array $elementAttributes = [], array $options = []): string
{
return $this->getLatte()->renderToString(
$this->templates->getTemplateFile(Templates::ClientSide),
new ClientSideTemplate($position, $elementAttributes, $options),
new ClientSideTemplate($position, $mode, $elementAttributes, $options),
);
}

Expand Down
5 changes: 5 additions & 0 deletions src/Renderer/Latte/Templates/ClientSideTemplate.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@

namespace SixtyEightPublishers\AmpClient\Renderer\Latte\Templates;

use SixtyEightPublishers\AmpClient\Renderer\ClientSideMode;
use SixtyEightPublishers\AmpClient\Request\ValueObject\Position;

final class ClientSideTemplate
{
public Position $position;

public ClientSideMode $mode;

/** @var array<string, scalar|null> */
public array $elementAttributes;

Expand All @@ -22,10 +25,12 @@ final class ClientSideTemplate
*/
public function __construct(
Position $position,
ClientSideMode $mode,
array $elementAttributes,
array $options
) {
$this->position = $position;
$this->mode = $mode;
$this->elementAttributes = $elementAttributes;
$this->options = $options;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Renderer/Latte/Templates/clientSide.latte
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
{var $helpers = SixtyEightPublishers\AmpClient\Renderer\Latte\Helpers::class}

<div data-amp-banner="{$position->getCode()}"
n:attr="array_merge($helpers::createResourceAttributes($position), $helpers::createOptionAttributes($options), $elementAttributes)">
n:attr="array_merge(['data-amp-mode' => $mode->isEmbed() ? $mode->getValue() : null], $helpers::createResourceAttributes($position), $helpers::createOptionAttributes($options), $elementAttributes)">
</div>
5 changes: 3 additions & 2 deletions src/Renderer/Phtml/PhtmlRendererBridge.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace SixtyEightPublishers\AmpClient\Renderer\Phtml;

use SixtyEightPublishers\AmpClient\Renderer\ClientSideMode;
use SixtyEightPublishers\AmpClient\Renderer\OutputBuffer;
use SixtyEightPublishers\AmpClient\Renderer\RendererBridgeInterface;
use SixtyEightPublishers\AmpClient\Renderer\Templates;
Expand Down Expand Up @@ -86,11 +87,11 @@ public function renderMultiple(ResponsePosition $position, array $banners, array
/**
* @throws Throwable
*/
public function renderClientSide(RequestPosition $position, array $elementAttributes = [], array $options = []): string
public function renderClientSide(RequestPosition $position, ClientSideMode $mode, array $elementAttributes = [], array $options = []): string
{
$filename = $this->templates->getTemplateFile(Templates::ClientSide);

return OutputBuffer::capture(function () use ($filename, $position, $elementAttributes, $options) {
return OutputBuffer::capture(function () use ($filename, $position, $elementAttributes, $options, $mode) {
require $filename;
});
}
Expand Down
11 changes: 9 additions & 2 deletions src/Renderer/Phtml/Templates/clientSide.phtml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@ declare(strict_types=1);

namespace SixtyEightPublishers\AmpClient\Renderer\Phtml;

use SixtyEightPublishers\AmpClient\Renderer\ClientSideMode;
use SixtyEightPublishers\AmpClient\Request\ValueObject\Position;
use SixtyEightPublishers\AmpClient\Request\ValueObject\BannerResource;

/** @var Position $position */
/** @var array<string, scalar|null> $elementAttributes */
/** @var array<string, scalar> $options */
/** @var ClientSideMode $mode */
?>
<div data-amp-banner="<?= Helpers::escapeHtmlAttr($position->getCode()) ?>"
<?php foreach ($position->getResources() as $resource) : ?><?= 'data-amp-resource-' . $resource->getCode() . '="' . implode(',', $resource->getValues()) . '"' ?><?php endforeach ?>
<?= Helpers::printAttributes(array_merge(Helpers::prefixKeys($options, 'data-amp-option-'), $elementAttributes)) ?>>
<?= Helpers::printAttributes(array_merge(
['data-amp-mode' => $mode->isEmbed() ? $mode->getValue() : null],
array_map(static fn (BannerResource $resource): string => implode(',', $resource->getValues()), Helpers::prefixKeys($position->getResources(), 'data-amp-resource-')),
Helpers::prefixKeys($options, 'data-amp-option-'),
$elementAttributes
)) ?>>
</div>
Loading

0 comments on commit 7b048ea

Please sign in to comment.