diff --git a/README.md b/README.md
index 14b2164..9e214e9 100644
--- a/README.md
+++ b/README.md
@@ -44,3 +44,106 @@ the contents of which needs to be deployed to the `BASE_URL`.
## Pages CI setup
Includes Gitlab CI / Gitlab Pages setup.
+
+## Features
+
+### Custom Social Images
+
+Generate dynamic social images (Open Graph, Twitter Cards) from SVG templates. Each route can render a `.svg` version that is then processed through ImgProxy.
+
+#### How It Works
+
+1. **SVG URLs**: Any Linkable entity can generate an SVG URL using `yassg_svg_url(entity)`
+2. **Template Rendering**: The SVG template at `pages/{route}.svg.twig` receives the same context as the HTML page
+3. **ImgProxy Processing**: Use `yassg_thumbnail()` to process the SVG through ImgProxy
+4. **Build Time**: Images are generated and optimized during the build process
+
+#### Usage in Templates
+
+Define a `social_image` block in your page template that uses `yassg_svg_url()`:
+
+```twig
+{# templates/pages/article.html.twig #}
+{% extends 'layout.html.twig' %}
+
+{% set article = yassg_find_one_by('articles', {condition: {'item.slug': slug}}) %}
+
+{% block title %}{{ article.title }}{% endblock %}
+
+{% block social_image %}{{ yassg_svg_url(article) }}{% endblock %}
+
+{% block body %}
+ {# ... #}
+{% endblock %}
+```
+
+In your layout template, use the block to generate meta tags:
+
+```twig
+{# templates/layout.html.twig #}
+{% block social_image %}{% endblock %}
+{% set social_image_url = block('social_image') %}
+{% if social_image_url is not empty %}
+
+
+
+{% endif %}
+```
+
+#### Creating SVG Templates
+
+Create SVG templates in `templates/pages/{route}.svg.twig`:
+
+```svg
+{# templates/pages/article.svg.twig #}
+
+```
+
+The SVG template receives the same variables as the HTML template (article, page, etc.).
+
+#### Making Your Model Linkable
+
+To use `yassg_svg_url()`, your model must implement the `Linkable` interface:
+
+```php
+use Sigwin\YASSG\Linkable;
+
+final class Article implements Linkable
+{
+ public string $title;
+ public string $slug;
+
+ public function getLinkRouteName(): string
+ {
+ return 'article';
+ }
+
+ public function getLinkRouteParameters(): array
+ {
+ return ['slug' => $this->slug];
+ }
+}
+```
+
+#### Composing with Other Assets
+
+Since SVG templates can use any Twig functions, you can include other assets:
+
+```svg
+
+```
+
diff --git a/composer.json b/composer.json
index ccb2b2a..2a01e7e 100644
--- a/composer.json
+++ b/composer.json
@@ -15,6 +15,7 @@
"presta/sitemap-bundle": "^4.0",
"spatie/commonmark-highlighter": "^3.0",
"symfony/console": "^6.4 || ^7.0",
+ "symfony/dependency-injection": "^6.4 || ^7.0",
"symfony/expression-language": "^6.4 || ^7.0",
"symfony/filesystem": "^6.4 || ^7.0",
"symfony/finder": "^6.4 || ^7.0",
diff --git a/config/routes/web_profiler.yaml b/config/routes/web_profiler.yaml
index 8d85319..b3b7b4b 100644
--- a/config/routes/web_profiler.yaml
+++ b/config/routes/web_profiler.yaml
@@ -1,8 +1,8 @@
when@dev:
web_profiler_wdt:
- resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
+ resource: '@WebProfilerBundle/Resources/config/routing/wdt.php'
prefix: /_wdt
web_profiler_profiler:
- resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
+ resource: '@WebProfilerBundle/Resources/config/routing/profiler.php'
prefix: /_profiler
diff --git a/config/routes/yassg.yaml b/config/routes/yassg.yaml
index 9611ba1..a9b686d 100644
--- a/config/routes/yassg.yaml
+++ b/config/routes/yassg.yaml
@@ -11,5 +11,5 @@ yassg:
when@dev:
_errors:
- resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
+ resource: '@FrameworkBundle/Resources/config/routing/errors.php'
prefix: /_error
diff --git a/resources/init/demo/src/Model/Page.php b/resources/init/demo/src/Model/Page.php
index 0affbfc..9e2df53 100644
--- a/resources/init/demo/src/Model/Page.php
+++ b/resources/init/demo/src/Model/Page.php
@@ -2,9 +2,23 @@
namespace App\Model;
-final class Page
+use Sigwin\YASSG\Linkable;
+
+final class Page implements Linkable
{
public string $slug;
public string $title;
public string $image;
+
+ #[\Override]
+ public function getLinkRouteName(): string
+ {
+ return 'demo';
+ }
+
+ #[\Override]
+ public function getLinkRouteParameters(): array
+ {
+ return [];
+ }
}
diff --git a/resources/init/demo/templates/layout.html.twig b/resources/init/demo/templates/layout.html.twig
index 101a8f6..6782782 100644
--- a/resources/init/demo/templates/layout.html.twig
+++ b/resources/init/demo/templates/layout.html.twig
@@ -7,6 +7,14 @@
{{ demo_page.title }}
+ {% block social_image %}{% endblock %}
+ {% set social_image_url = block('social_image') %}
+ {% if social_image_url is not empty %}
+
+
+
+ {% endif %}
+
{{ encore_entry_link_tags('app') }}
diff --git a/resources/init/demo/templates/pages/demo.html.twig b/resources/init/demo/templates/pages/demo.html.twig
index 76d86af..a4643a6 100644
--- a/resources/init/demo/templates/pages/demo.html.twig
+++ b/resources/init/demo/templates/pages/demo.html.twig
@@ -3,6 +3,8 @@
{# query the database #}
{% set demo_page = yassg_find_one_by('pages', {condition: {'item.slug': 'demo-page'}}) %}
+{% block social_image %}{{ yassg_svg_url(demo_page) }}{% endblock %}
+
{% block body %}
diff --git a/resources/init/demo/templates/pages/demo.svg.twig b/resources/init/demo/templates/pages/demo.svg.twig
new file mode 100644
index 0000000..f2b6a9b
--- /dev/null
+++ b/resources/init/demo/templates/pages/demo.svg.twig
@@ -0,0 +1,30 @@
+
diff --git a/src/Bridge/Symfony/Controller/DefaultController.php b/src/Bridge/Symfony/Controller/DefaultController.php
index d59c994..d147b3b 100644
--- a/src/Bridge/Symfony/Controller/DefaultController.php
+++ b/src/Bridge/Symfony/Controller/DefaultController.php
@@ -31,9 +31,23 @@ public function __invoke(RequestStack $requestStack): Response
throw new \LogicException('Invalid request, invalid route attribute');
}
+ /** @var null|string $filename */
+ $filename = $request->attributes->get('_filename');
+ switch (true) {
+ case $filename === 'index.svg':
+ $type = 'svg';
+ $contentType = 'image/svg+xml';
+ break;
+ default:
+ $type = 'html';
+ $contentType = 'text/html; charset=UTF-8';
+ }
+
/** @var string $template */
- $template = $request->attributes->get('_template') ?? \sprintf('pages/%1$s.html.twig', $route);
+ $template = $request->attributes->get('_template') ?? \sprintf('pages/%1$s.%2$s.twig', $route, $type);
+ $response = $this->render($template, $request->attributes->all());
+ $response->headers->set('Content-Type', $contentType);
- return $this->render($template, $request->attributes->all());
+ return $response;
}
}
diff --git a/src/Bridge/Symfony/Routing/Loader/RouteLoader.php b/src/Bridge/Symfony/Routing/Loader/RouteLoader.php
index 085d7de..40d7c14 100644
--- a/src/Bridge/Symfony/Routing/Loader/RouteLoader.php
+++ b/src/Bridge/Symfony/Routing/Loader/RouteLoader.php
@@ -35,9 +35,10 @@ public function __invoke(): RouteCollection
$path = $route['path'];
$requirements = [];
} else {
+ // Support both index.html and .svg extension
$path = $route['path'].'/{_filename}';
$requirements = [
- '_filename' => 'index\.html',
+ '_filename' => 'index(\.html|\.svg)',
];
}
diff --git a/src/Bridge/Twig/Extension/ImageExtension.php b/src/Bridge/Twig/Extension/ImageExtension.php
index eace757..eaec3d1 100644
--- a/src/Bridge/Twig/Extension/ImageExtension.php
+++ b/src/Bridge/Twig/Extension/ImageExtension.php
@@ -244,6 +244,10 @@ private function buildImgproxyFilter(array $options): string
private function buildImgproxyUrl(string $path, string $filters): string
{
- return \sprintf('%1$s/insecure/%2$s%3$s', $this->imgproxyUrl, $filters, $this->encode('local:///'.mb_ltrim(str_replace($GLOBALS['YASSG_BASEDIR'], '', $path), '/')));
+ if (! str_contains($path, '://')) {
+ $path = 'local:///'.mb_ltrim(str_replace($GLOBALS['YASSG_BASEDIR'], '', $path), '/');
+ }
+
+ return \sprintf('%1$s/insecure/%2$s%3$s', $this->imgproxyUrl, $filters, $this->encode($path));
}
}
diff --git a/src/Bridge/Twig/Extension/LinkableExtension.php b/src/Bridge/Twig/Extension/LinkableExtension.php
index 9346811..e8a408b 100644
--- a/src/Bridge/Twig/Extension/LinkableExtension.php
+++ b/src/Bridge/Twig/Extension/LinkableExtension.php
@@ -28,7 +28,8 @@ public function __construct(private readonly UrlGeneratorInterface $generator)
public function getFunctions(): array
{
return [
- new TwigFunction('yassg_url', fn (Linkable $linkable) => $this->generator->generate($linkable->getLinkRouteName(), $linkable->getLinkRouteParameters())),
+ new TwigFunction('yassg_url', fn (Linkable $linkable, array $parameters = []) => $this->generator->generate($linkable->getLinkRouteName(), array_replace($linkable->getLinkRouteParameters(), $parameters))),
+ new TwigFunction('yassg_svg_url', fn (Linkable $linkable, array $parameters = []) => $this->generator->generate($linkable->getLinkRouteName(), array_replace($linkable->getLinkRouteParameters(), $parameters, ['_filename' => 'index.svg']))),
];
}
}
diff --git a/tests/functional/site/compose.yaml b/tests/functional/site/compose.yaml
index c70cb07..bbf439a 100644
--- a/tests/functional/site/compose.yaml
+++ b/tests/functional/site/compose.yaml
@@ -1,15 +1,18 @@
services:
app:
- image: php:8.2-alpine
+ image: php:8.4-alpine
environment:
BASE_URL: ${BASE_URL}
- working_dir: /app
+ YASSG_SKIP_BUNDLES: Symfony\WebpackEncoreBundle\WebpackEncoreBundle
+ command: php -S 0.0.0.0:9988 -t /app/tests/functional/site
+ ports:
+ - "9988:9988"
volumes:
- - .:/app
+ - ../../../:/app
tmpfs:
- /tmp
webpack:
- image: node:20-alpine
+ image: node:25-alpine
environment:
BASE_URL: ${BASE_URL}
working_dir: /app
@@ -20,13 +23,14 @@ services:
imgproxy:
image: darthsim/imgproxy:v3.27.2
environment:
+ - IMGPROXY_BIND=:8090
- IMGPROXY_LOCAL_FILESYSTEM_ROOT=/app
- IMGPROXY_MAX_ANIMATION_FRAMES=60
- - IMGPROXY_ENABLE_AVIF_DETECTION=1
- - IMGPROXY_ENABLE_WEBP_DETECTION=1
+ - IMGPROXY_AUTO_AVIF=1
+ - IMGPROXY_AUTO_WEBP=1
working_dir: /app
ports:
- - 8090:8080
+ - 8090:8090
volumes:
- .:/app:ro
tmpfs:
diff --git a/tests/functional/site/templates/layout.html.twig b/tests/functional/site/templates/layout.html.twig
index 3e01f73..e8050dd 100644
--- a/tests/functional/site/templates/layout.html.twig
+++ b/tests/functional/site/templates/layout.html.twig
@@ -6,7 +6,15 @@
{% block title %}{% endblock %}
-
+
+ {% block social_image %}{% endblock %}
+ {% set social_image_url = block('social_image') %}
+ {% if social_image_url is not empty %}
+
+
+
+ {% endif %}
+
{{ encore_entry_link_tags('app') }}
diff --git a/tests/functional/site/templates/pages/article.html.twig b/tests/functional/site/templates/pages/article.html.twig
index 52f1e9f..697b243 100644
--- a/tests/functional/site/templates/pages/article.html.twig
+++ b/tests/functional/site/templates/pages/article.html.twig
@@ -5,6 +5,8 @@
{% block title %}{{ article.title }}{% endblock %}
+{% block social_image %}{{ yassg_svg_url(article) }}{% endblock %}
+
{% block body %}
diff --git a/tests/functional/site/templates/pages/article.svg.twig b/tests/functional/site/templates/pages/article.svg.twig
new file mode 100644
index 0000000..e44aa21
--- /dev/null
+++ b/tests/functional/site/templates/pages/article.svg.twig
@@ -0,0 +1,11 @@
+{% set article = yassg_find_one_by('articles', {condition: {'item.slug': slug}}) %}
+
+