Skip to content

Commit 9101d1f

Browse files
Merge pull request #831 from flexponsive/pr/blade-inline-as-json
Add JSON option to `@routes` for improved CSP compatibility
2 parents 9cdd300 + a1ed1b9 commit 9101d1f

File tree

8 files changed

+87
-2
lines changed

8 files changed

+87
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ node_modules/
22
vendor/
33
*.cache
44
*.log
5+
*vitest-temp*
56
composer.lock
67
phpunit.xml

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,12 @@ A [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CS
546546
@routes(nonce: 'your-nonce-here')
547547
```
548548

549+
Alternatively, you can configure Ziggy to output your routes as plain JSON, rather than JavaScript, so that the output is ignored by the CSP. Note that if you use this option you will need to load Ziggy's JavaScript `route()` function yourself, by configuring the Vue plugin or React hook or importing the JavaScript manually.
550+
551+
```php
552+
@routes(json: true)
553+
```
554+
549555
### Disabling the `route()` helper
550556

551557
If you only want to use the `@routes` directive to make Ziggy's configuration available in JavaScript, but don't need the `route()` helper function, set the `ziggy.skip-route-function` config to `true`.

src/BladeRouteGenerator.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,24 @@
22

33
namespace Tighten\Ziggy;
44

5+
use Tighten\Ziggy\Output\Json;
56
use Tighten\Ziggy\Output\MergeScript;
67
use Tighten\Ziggy\Output\Script;
78

89
class BladeRouteGenerator
910
{
1011
public static $generated;
1112

12-
public function generate(array|string|null $group = null, ?string $nonce = null): string
13+
public function generate(array|string|null $group = null, ?string $nonce = null, ?bool $json = false): string
1314
{
1415
$ziggy = new Ziggy($group);
1516

17+
if ($json) {
18+
$output = config('ziggy.output.json', Json::class);
19+
20+
return (string) new $output($ziggy);
21+
}
22+
1623
$nonce = $nonce ? " nonce=\"{$nonce}\"" : '';
1724

1825
if (static::$generated) {

src/Output/Json.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace Tighten\Ziggy\Output;
4+
5+
use Stringable;
6+
use Tighten\Ziggy\Ziggy;
7+
8+
class Json implements Stringable
9+
{
10+
public function __construct(
11+
protected Ziggy $ziggy,
12+
) {}
13+
14+
public function __toString(): string
15+
{
16+
return <<<HTML
17+
<script id="ziggy-routes-json" type="application/json">{$this->ziggy->toJson()}</script>
18+
HTML;
19+
}
20+
}

src/js/Router.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ export default class Router extends String {
1515
super();
1616

1717
this._config = config ?? (typeof Ziggy !== 'undefined' ? Ziggy : globalThis?.Ziggy);
18+
19+
if (
20+
!this._config &&
21+
typeof document !== 'undefined' &&
22+
document.getElementById('ziggy-routes-json')
23+
) {
24+
globalThis.Ziggy = JSON.parse(document.getElementById('ziggy-routes-json').textContent);
25+
this._config = globalThis.Ziggy;
26+
}
27+
1828
this._config = { ...this._config, absolute };
1929

2030
if (name) {

src/js/index.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ export const ZiggyVue = {
2525
};
2626

2727
export function useRoute(defaultConfig) {
28-
if (!defaultConfig && !globalThis.Ziggy && typeof Ziggy === 'undefined') {
28+
if (
29+
!defaultConfig &&
30+
!globalThis.Ziggy &&
31+
typeof Ziggy === 'undefined' &&
32+
!document.getElementById('ziggy-routes-json')
33+
) {
2934
throw new Error(
3035
'Ziggy error: missing configuration. Ensure that a `Ziggy` variable is defined globally or pass a config object into the useRoute hook.',
3136
);

tests/Unit/BladeRouteGeneratorTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@
9393
->toContain('<script type="text/javascript" nonce="test-nonce">');
9494
});
9595

96+
test('render JSON', function () {
97+
expect((new BladeRouteGenerator)->generate(json: true))
98+
->toContain('<script id="ziggy-routes-json" type="application/json">')
99+
->not->toContain('function');
100+
});
101+
96102
test('render script tag', function () {
97103
Route::get('posts', fn () => '')->name('posts.index');
98104

tests/js/route.test.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1503,3 +1503,33 @@ describe('current()', () => {
15031503
global.window = oldWindow;
15041504
});
15051505
});
1506+
1507+
describe('json', () => {
1508+
test('generate a URL with routes loaded from JSON', () => {
1509+
let script = document.createElement('script');
1510+
script.id = 'ziggy-routes-json';
1511+
script.type = 'application/json';
1512+
script.textContent = JSON.stringify(defaultZiggy);
1513+
document.head.appendChild(script);
1514+
global.Ziggy = undefined;
1515+
1516+
expect(route('posts.index')).toBe('https://ziggy.dev/posts');
1517+
1518+
document.head.removeChild(script);
1519+
});
1520+
1521+
test('only parse JSON routes once', () => {
1522+
let script = document.createElement('script');
1523+
script.id = 'ziggy-routes-json';
1524+
script.type = 'application/json';
1525+
script.textContent = JSON.stringify(defaultZiggy);
1526+
document.head.appendChild(script);
1527+
global.Ziggy = undefined;
1528+
1529+
expect(route('posts.index')).toBe('https://ziggy.dev/posts');
1530+
1531+
document.head.removeChild(script);
1532+
1533+
expect(route('posts.show', 1)).toBe('https://ziggy.dev/posts/1');
1534+
});
1535+
});

0 commit comments

Comments
 (0)