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 #} + + + + {{ article.title }} + + + {{ article.publishedAt|date('F j, Y') }} + + +``` + +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 + + + + + {{ article.title }} + + +``` + 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 @@ + + + + + + + + + + + + + {% if demo_page is defined %} + {{ demo_page.title }} + {% else %} + My Awesome Site + {% endif %} + + + + {% if demo_page is defined and demo_page.description is defined %} + {{ demo_page.description }} + {% else %} + Built with YASSG + {% endif %} + + 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}}) %} + + + + + {{ article.title }} + + + {{ article.publishedAt|date('F j, Y') }} + +