Skip to content

Commit e168103

Browse files
committed
Implement Blog RSS
1 parent 3ddc662 commit e168103

30 files changed

+281
-25
lines changed
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace App\Http\Controllers;
4+
5+
use Illuminate\Http\Response;
6+
use Statamic\Facades\Entry;
7+
8+
class BlogController extends Controller
9+
{
10+
public function feed(): Response
11+
{
12+
$posts = Entry::whereCollection('posts')->sortByDesc('publication_date')->all();
13+
14+
return response()
15+
->view('blog.rss', ['posts' => $posts])
16+
->header('Content-Type', 'application/xml');
17+
}
18+
19+
public function styles(): Response
20+
{
21+
$content = file_get_contents(resource_path('assets/xsl/feed.xsl'));
22+
23+
if (empty($content)) {
24+
abort(404);
25+
}
26+
27+
return response($content)->header('Content-Type', 'text/xml');
28+
}
29+
}

app/Support/helpers.php

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
use Illuminate\Support\Arr;
4+
use Statamic\Facades\Entry;
5+
use Statamic\Facades\GlobalSet;
6+
use Statamic\Statamic;
7+
use Symfony\Component\Routing\Exception\RouteNotFoundException;
8+
9+
if (! function_exists('sroute')) {
10+
11+
/**
12+
* @param string[]|string $parameters
13+
*/
14+
function sroute(string $name, array|string $parameters = [], bool $absolute = true)
15+
{
16+
$entry = empty($parameters) ? Entry::find($name) : null;
17+
18+
if (is_null($entry)) {
19+
$path = '/' . implode('/', array_merge([$name], Arr::wrap($parameters)));
20+
$entry = Entry::findByUri($path);
21+
}
22+
23+
if (is_null($entry)) {
24+
throw new RouteNotFoundException("Statamic Route [{$name}] not found.");
25+
}
26+
27+
if ($absolute) {
28+
return strval(Statamic::tag('link')->to($entry->url())->absolute(true));
29+
}
30+
31+
return $entry->url();
32+
}
33+
34+
}
35+
36+
if (! function_exists('sglobal')) {
37+
38+
function sglobal(string $key)
39+
{
40+
[$namespace, $key] = explode('.', $key);
41+
$set = GlobalSet::findByHandle($namespace);
42+
43+
if (is_null($set)) {
44+
throw new Exception("Statamic Global [{$key}] not found.");
45+
}
46+
47+
$variables = $set->inCurrentSite();
48+
49+
if (is_null($variables)) {
50+
throw new Exception("Statamic Global [{$key}] not found.");
51+
}
52+
53+
if (! $variables->has($key)) {
54+
throw new Exception("Statamic Global [{$key}] not found.");
55+
}
56+
57+
return $variables->get($key);
58+
}
59+
60+
}

bootstrap/app.php

-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
return Application::configure(basePath: dirname(__DIR__))
88
->withRouting(
99
web: __DIR__ . '/../routes/web.php',
10-
commands: __DIR__ . '/../routes/console.php',
1110
health: '/up',
1211
)
1312
->withMiddleware(function (Middleware $middleware) {

composer.json

+3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
"tightenco/duster": "^3.1"
2929
},
3030
"autoload": {
31+
"files": [
32+
"app/Support/helpers.php"
33+
],
3134
"psr-4": {
3235
"App\\": "app/",
3336
"Database\\Factories\\": "database/factories/",

phpstan.neon

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ parameters:
66
- app/
77
excludePaths:
88
- app/Services/ActivityService.php
9+
- app/Support/helpers.php
File renamed without changes.
File renamed without changes.

resources/assets/css/main.css

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
@import 'tailwindcss';
2+
3+
@config '../../../tailwind.config.js';
4+
5+
@source "../../views";
6+
@source "../../../app";
7+
@source "../../../content";
8+
9+
@layer base {
10+
@import './base.css';
11+
@import './fonts.css';
12+
}
File renamed without changes.

resources/assets/xsl/feed.xsl

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<xsl:stylesheet
3+
version="1.0"
4+
xmlns="http://www.w3.org/1999/xhtml"
5+
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
6+
xmlns:atom="http://www.w3.org/2005/Atom">
7+
<xsl:output method="html" indent="no" omit-xml-declaration="yes"/>
8+
<xsl:template match="atom:feed">
9+
<html>
10+
<head>
11+
<meta charset="utf-8" />
12+
<meta name="viewport" content="width=device-width, initial-scale=1" />
13+
<title><xsl:value-of select="title" /></title>
14+
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
15+
</head>
16+
<body class="bg-gray-50">
17+
<div class="max-w-prose px-6 py-8 mx-auto">
18+
<div class="flex items-center">
19+
<div class="p-2 rounded" style="background:#ff6600">
20+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 430.117 430.118" class="size-6 text-white">
21+
<path style="fill:currentColor" d="M97.493,332.473c10.419,10.408,16.755,24.525,16.794,40.244c-0.04,15.687-6.375,29.809-16.755,40.17l-0.04,0.019
22+
c-10.398,10.352-24.603,16.681-40.398,16.681c-15.775,0-29.944-6.348-40.34-16.699C6.384,402.526,0,388.422,0,372.717
23+
c0-15.719,6.384-29.869,16.754-40.253v0.009c10.401-10.36,24.57-16.735,40.34-16.735C72.89,315.738,87.081,322.131,97.493,332.473z
24+
M97.493,332.464v0.009c0.019,0,0.019,0,0.019,0L97.493,332.464z M16.754,412.906c0,0,0,0,0-0.019c-0.019,0-0.019,0-0.019,0
25+
L16.754,412.906z M0.046,146.259v82.129c53.618,0.033,104.328,21.096,142.278,59.104c37.943,37.888,58.917,88.675,59.003,142.477
26+
h0.028v0.149h82.467c-0.065-78.233-31.866-149.099-83.279-200.549C149.122,178.126,78.285,146.308,0.046,146.259z M0.196,0v82.089
27+
c191.661,0.14,347.464,156.184,347.594,348.028h82.327c-0.056-118.571-48.248-225.994-126.132-303.932
28+
C226.073,48.274,118.721,0.051,0.196,0z" />
29+
</svg>
30+
</div>
31+
32+
<h1 class="ml-2 font-semibold text-3xl"><xsl:value-of select="atom:title"/></h1>
33+
</div>
34+
35+
<p class="mt-4"><xsl:value-of select="atom:subtitle" /></p>
36+
37+
<div class="space-y-4 pt-2">
38+
<xsl:for-each select="atom:entry">
39+
<div class="bg-white border px-8 py-4 rounded text-sm mt-2 prose">
40+
<h2><xsl:value-of select="atom:title"/></h2>
41+
<xsl:value-of select="atom:summary" disable-output-escaping="yes" />
42+
<br></br>
43+
<a>
44+
<xsl:attribute name="href">
45+
<xsl:value-of select="link/@href"/>
46+
</xsl:attribute>
47+
Read more
48+
</a>
49+
</div>
50+
</xsl:for-each>
51+
</div>
52+
</div>
53+
</body>
54+
</html>
55+
</xsl:template>
56+
</xsl:stylesheet>

resources/blueprints/collections/posts/post.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,9 @@ tabs:
3232
display: 'Publication date'
3333
validate:
3434
- required
35+
- handle: modification_date
36+
field:
37+
type: date
38+
time_enabled: true
39+
time_seconds_enabled: true
40+
display: 'Modification date'

resources/css/main.css

-12
This file was deleted.

resources/views/blog/index.blade.php

+24
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,33 @@
33
@section('main')
44
<h1 class="sr-only">Blog</h1>
55

6+
<a
7+
class="bg-rss float-right hidden items-center justify-center rounded p-1 text-sm text-white no-underline opacity-75 hover:text-white hover:opacity-100 focus:opacity-100 md:flex"
8+
href="{{ route('blog.rss') }}"
9+
title="Open RSS feed"
10+
target="_blank"
11+
>
12+
<s:partial src="icons/rss" class="inline h-4 fill-current text-white" />
13+
</a>
14+
615
<s:collection :from="$mount" sort="publication_date:desc">
716
<x-content-card :$title :$url :date="$publication_date">
817
{!! $summary !!}
918
</x-content-card>
1019
</s:collection>
20+
21+
<div class="flex justify-end">
22+
<a
23+
class="bg-rss flex items-center rounded p-2 text-white no-underline opacity-75 hover:opacity-100 md:hidden"
24+
href="{{ route('blog.rss') }}"
25+
title="Open RSS feed"
26+
target="_blank"
27+
>
28+
<span class="mr-1 text-white">Subscribe</span>
29+
<s:partial
30+
src="icons/rss"
31+
class="inline h-3 fill-current text-white"
32+
/>
33+
</a>
34+
</div>
1135
@endsection

resources/views/blog/rss.blade.php

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php echo '<?xml version="1.0" encoding="utf-8" ?>'; ?>
2+
3+
<?php echo '<?xml-stylesheet type="text/xsl" href="' .
4+
route('blog.xsl') .
5+
'" ?>' .
6+
"\n"; ?>
7+
8+
<feed xmlns="http://www.w3.org/2005/Atom">
9+
<title type="text">Noel De Martin</title>
10+
<subtitle type="text">
11+
Problem Solver, Software Architect, Entrepreneur
12+
</subtitle>
13+
<updated>
14+
{!! $posts[0]->publication_date->format(DateTime::ATOM) !!}
15+
</updated>
16+
<id>{!! sroute('blog') !!}</id>
17+
<link type="text/html" href="{!! sroute('blog') !!}" />
18+
<link type="application/xml" rel="self" href="{!! route('blog.rss') !!}" />
19+
<category term="entrepreneurship" />
20+
<category term="development" />
21+
<icon>{!! asset('favicon.ico') !!}</icon>
22+
<logo>{!! asset('img/myface-small.png') !!}</logo>
23+
<author>
24+
<name>Noel De Martin</name>
25+
<email>{{ sglobal('contact.email') }}</email>
26+
<uri>{!! sroute('home') !!}</uri>
27+
</author>
28+
@foreach ($posts as $post)
29+
<entry>
30+
<title type="text">{!! $post->title !!}</title>
31+
<author>
32+
<name>Noel De Martin</name>
33+
<email>{{ sglobal('contact.email') }}</email>
34+
<uri>{!! sroute('home') !!}</uri>
35+
</author>
36+
<link href="{!! sroute('blog', $post->slug) !!}" />
37+
<id>{!! sroute('blog', $post->slug) !!}</id>
38+
<published>
39+
{!! $post->publication_date->format(DateTime::ATOM) !!}
40+
</published>
41+
<updated>
42+
{!! ($post->modification_date ?? $post->publication_date)->format(DateTime::ATOM) !!}
43+
</updated>
44+
<summary type="html">
45+
{!! htmlspecialchars($post->summary) !!}
46+
</summary>
47+
<content
48+
type="html"
49+
xml:base="{!! sroute('blog', $post->slug) !!}"
50+
>
51+
{!! htmlspecialchars($post->content) !!}
52+
</content>
53+
</entry>
54+
@endforeach
55+
</feed>

resources/views/blog/show.blade.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,6 @@ class="text-blue-darker ml-2 flex items-center text-xs font-normal"
3232
</article>
3333

3434
<div class="my-8 text-left md:text-right">
35-
<a href="/blog">Read more posts →</a>
35+
<a href="{{ sroute('blog') }}">Read more posts →</a>
3636
</div>
3737
@endsection

resources/views/components/layout-header.blade.php

+7-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33
class="z-10 flex h-16 flex-col items-center md:h-32 lg:h-40"
44
>
55
<div class="max-w-content flex w-full overflow-hidden">
6-
<a href="/" aria-label="Home" title="Home" class="flex" tabindex="-1">
6+
<a
7+
href="{{ sroute("home") }}"
8+
aria-label="Home"
9+
title="Home"
10+
class="flex"
11+
tabindex="-1"
12+
>
713
<img
814
src="/img/myface.png"
915
alt=""
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<svg
2+
class="{{ class ?? '' }}"
3+
xmlns="http://www.w3.org/2000/svg"
4+
viewBox="0 0 430.117 430.118"
5+
>
6+
<path
7+
id="RSS"
8+
d="M97.493,332.473c10.419,10.408,16.755,24.525,16.794,40.244c-0.04,15.687-6.375,29.809-16.755,40.17l-0.04,0.019 c-10.398,10.352-24.603,16.681-40.398,16.681c-15.775,0-29.944-6.348-40.34-16.699C6.384,402.526,0,388.422,0,372.717 c0-15.719,6.384-29.869,16.754-40.253v0.009c10.401-10.36,24.57-16.735,40.34-16.735C72.89,315.738,87.081,322.131,97.493,332.473z M97.493,332.464v0.009c0.019,0,0.019,0,0.019,0L97.493,332.464z M16.754,412.906c0,0,0,0,0-0.019c-0.019,0-0.019,0-0.019,0 L16.754,412.906z M0.046,146.259v82.129c53.618,0.033,104.328,21.096,142.278,59.104c37.943,37.888,58.917,88.675,59.003,142.477 h0.028v0.149h82.467c-0.065-78.233-31.866-149.099-83.279-200.549C149.122,178.126,78.285,146.308,0.046,146.259z M0.196,0v82.089 c191.661,0.14,347.464,156.184,347.594,348.028h82.327c-0.056-118.571-48.248-225.994-126.132-303.932 C226.073,48.274,118.721,0.051,0.196,0z"
9+
/>
10+
</svg>

resources/views/layout.blade.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
display: var(--default-display);
1919
}
2020
</style>
21-
@vite(['resources/css/main.css', 'resources/js/main.js'])
21+
@vite(['resources/assets/css/main.css', 'resources/assets/js/main.js'])
2222
</head>
2323
<body
2424
class="font-ubuntu text-black-light flex min-h-screen flex-col bg-white text-base leading-tight font-normal antialiased print:block"

routes/console.php

-8
This file was deleted.

routes/web.php

+4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
<?php
22

3+
use App\Http\Controllers\BlogController;
34
use Illuminate\Support\Facades\Route;
45

6+
Route::get('blog/rss.xml', [BlogController::class, 'feed'])->name('blog.rss');
7+
Route::get('blog/styles.xsl', [BlogController::class, 'styles'])->name('blog.xsl');
8+
59
Route::redirect('solid-world', 'https://www.youtube.com/watch?v=cajBTJXmKhA');
610
Route::redirect('fosdem', 'https://www.youtube.com/watch?v=kPzhykRVDuI');
711
Route::redirect('solid-symposium-dx', 'https://www.youtube.com/watch?v=ghGmveKKe5Y');

tests/Feature/NavigationTest.php

+8
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@
2929
$response->assertSee('What this blog will be about');
3030
});
3131

32+
test('Blog RSS', function () {
33+
$response = $this->get('/blog/rss.xml');
34+
35+
$response->assertStatus(200);
36+
$response->assertSee('<feed xmlns="http://www.w3.org/2005/Atom">', false);
37+
$response->assertSee('Starting Something New');
38+
});
39+
3240
test('Projects', function () {
3341
$response = $this->get('/projects');
3442

vite.config.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ export default defineConfig({
66
plugins: [
77
tailwindcss(),
88
laravel({
9-
input: ['resources/css/main.css', 'resources/js/main.js'],
9+
input: [
10+
'resources/assets/css/main.css',
11+
'resources/assets/js/main.js',
12+
],
1013
refresh: true,
1114
}),
1215
],

0 commit comments

Comments
 (0)