blog posts

This commit is contained in:
2025-05-25 08:59:30 +01:00
parent 87722cb95e
commit 20e604cf68
20 changed files with 509 additions and 123 deletions

View File

@@ -12,6 +12,7 @@
"doctrine/doctrine-migrations-bundle": "^3.4", "doctrine/doctrine-migrations-bundle": "^3.4",
"doctrine/orm": "^3.3", "doctrine/orm": "^3.3",
"league/commonmark": "^2.7", "league/commonmark": "^2.7",
"league/html-to-markdown": "^5.1",
"phpdocumentor/reflection-docblock": "^5.6", "phpdocumentor/reflection-docblock": "^5.6",
"phpstan/phpdoc-parser": "^2.1", "phpstan/phpdoc-parser": "^2.1",
"symfony/asset": "7.2.*", "symfony/asset": "7.2.*",

91
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "c3cbc37c53092d5fb23e7a7e21304c25", "content-hash": "194a6d80a9896ce73994dc1b6c059f1d",
"packages": [ "packages": [
{ {
"name": "composer/semver", "name": "composer/semver",
@@ -1635,6 +1635,95 @@
], ],
"time": "2022-12-11T20:36:23+00:00" "time": "2022-12-11T20:36:23+00:00"
}, },
{
"name": "league/html-to-markdown",
"version": "5.1.1",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/html-to-markdown.git",
"reference": "0b4066eede55c48f38bcee4fb8f0aa85654390fd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/0b4066eede55c48f38bcee4fb8f0aa85654390fd",
"reference": "0b4066eede55c48f38bcee4fb8f0aa85654390fd",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-xml": "*",
"php": "^7.2.5 || ^8.0"
},
"require-dev": {
"mikehaertl/php-shellcommand": "^1.1.0",
"phpstan/phpstan": "^1.8.8",
"phpunit/phpunit": "^8.5 || ^9.2",
"scrutinizer/ocular": "^1.6",
"unleashedtech/php-coding-standard": "^2.7 || ^3.0",
"vimeo/psalm": "^4.22 || ^5.0"
},
"bin": [
"bin/html-to-markdown"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.2-dev"
}
},
"autoload": {
"psr-4": {
"League\\HTMLToMarkdown\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Colin O'Dell",
"email": "colinodell@gmail.com",
"homepage": "https://www.colinodell.com",
"role": "Lead Developer"
},
{
"name": "Nick Cernis",
"email": "nick@cern.is",
"homepage": "http://modernnerd.net",
"role": "Original Author"
}
],
"description": "An HTML-to-markdown conversion helper for PHP",
"homepage": "https://github.com/thephpleague/html-to-markdown",
"keywords": [
"html",
"markdown"
],
"support": {
"issues": "https://github.com/thephpleague/html-to-markdown/issues",
"source": "https://github.com/thephpleague/html-to-markdown/tree/5.1.1"
},
"funding": [
{
"url": "https://www.colinodell.com/sponsor",
"type": "custom"
},
{
"url": "https://www.paypal.me/colinpodell/10.00",
"type": "custom"
},
{
"url": "https://github.com/colinodell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/league/html-to-markdown",
"type": "tidelift"
}
],
"time": "2023-07-12T21:21:09+00:00"
},
{ {
"name": "monolog/monolog", "name": "monolog/monolog",
"version": "3.9.0", "version": "3.9.0",

View File

@@ -18,6 +18,7 @@ services:
exclude: exclude:
- '../src/DependencyInjection/' - '../src/DependencyInjection/'
- '../src/Entity/' - '../src/Entity/'
- '../src/Interface/'
- '../src/Kernel.php' - '../src/Kernel.php'
# add more service definitions when explicit configuration is needed # add more service definitions when explicit configuration is needed

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace DoctrineMigrations; namespace DoctrineMigrations;
use DateTime;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
@@ -38,17 +39,21 @@ final class Version20250522212300 extends AbstractMigration
$blogPostPath = 'scripts/blog-migrated/' . $slug . '.yaml'; $blogPostPath = 'scripts/blog-migrated/' . $slug . '.yaml';
$blogPost = Yaml::parseFile($blogPostPath); $blogPost = Yaml::parseFile($blogPostPath);
$publishedDate = $blogPost['pubDate']; $publishedDateTime = DateTime::createFromFormat('U', strval($blogPost['pubDate']));
$updatedDate = array_key_exists(array: $blogPost, key: 'updatedDate') $publishedDate = $publishedDateTime->format('Y-m-d');
? $blogPost['updatedDate'] $updatedDateTime = array_key_exists(array: $blogPost, key: 'updatedDate')
? DateTime::createFromFormat('U', strval($blogPost['updatedDate']))
: null; : null;
$updatedDate = $updatedDateTime == null
? 'NULL'
: "'" . $updatedDateTime->format('Y-m-d') . "'";
$title = str_replace("'", "''", $blogPost['title']); $title = str_replace("'", "''", $blogPost['title']);
$description = str_replace("'", "''", $blogPost['description']); $description = str_replace("'", "''", $blogPost['description']);
$content = str_replace("'", "''", $blogPost['content']); $content = str_replace("'", "''", $blogPost['content']);
$this->addSql(<<<SQL $this->addSql(<<<SQL
INSERT INTO blog_post(slug, published_date, updated_date, title, description, content) INSERT INTO blog_post(slug, published_date, updated_date, title, description, content)
VALUES('$slug', '$publishedDate', '$updatedDate', '$title', '$description', '$content') VALUES('$slug', '$publishedDate', $updatedDate, '$title', '$description', '$content')
SQL); SQL);
} }
} }

View File

@@ -27,7 +27,7 @@
--colour-primary-fg: var(--colour-primary-90); --colour-primary-fg: var(--colour-primary-90);
--colour-primary-fg-accent: var(--colour-primary-80); --colour-primary-fg-accent: var(--colour-primary-80);
--colour-primary-bg: var(--colour-primary-10); --colour-primary-bg: var(--colour-primary-10);
--colour-primary-fg-accent: var(--colour-primary-20); --colour-primary-bg-accent: var(--colour-primary-20);
--colour-code-fg: var(--colour-primary-90); --colour-code-fg: var(--colour-primary-90);
--colour-code-bg: var(--colour-primary-15); --colour-code-bg: var(--colour-primary-15);
--colour-hyperlink: var(--colour-hyperlink-80); --colour-hyperlink: var(--colour-hyperlink-80);
@@ -56,7 +56,7 @@
--colour-primary-fg: var(--colour-primary-20); --colour-primary-fg: var(--colour-primary-20);
--colour-primary-fg-accent: var(--colour-primary-40); --colour-primary-fg-accent: var(--colour-primary-40);
--colour-primary-bg: var(--colour-primary-95); --colour-primary-bg: var(--colour-primary-95);
--colour-primary-fg-accent: var(--colour-primary-90); --colour-primary-bg-accent: var(--colour-primary-90);
--colour-hyperlink: var(--colour-hyperlink-40); --colour-hyperlink: var(--colour-hyperlink-40);
} }
} }
@@ -162,13 +162,13 @@ h3, h4, h5, h6 {
/** Hyperlinks */ /** Hyperlinks */
a:is(:link, :visited) { :is(:link, :visited) {
color: var(--colour-hyperlink); color: var(--colour-hyperlink);
text-decoration: underline; text-decoration: underline;
display: inline; display: inline;
} }
a:hover { :hover {
text-decoration: wavy; text-decoration: wavy;
} }

View File

@@ -1,3 +1,9 @@
p:has(.dt-published, .dt-updated) {
font-size: var(--font-size-sm);
font-style: italic;
margin-block-start: 0;
}
.p-summary { .p-summary {
font-style: italic; font-style: italic;
font-size: var(--font-size-sm); font-size: var(--font-size-sm);

View File

@@ -1,23 +1,76 @@
.skip-to { h2 {
display: inline-block; margin-block-start: var(--spacing-block-md);
margin-block-start: var(--spacing-block-xs);
} }
.skip-to ul { h3 {
margin-block-start: var(--spacing-block-xs); margin-block-start: var(--spacing-block-sm);
}
.skip-to :is(ul, li) {
display: inline-block;
list-style-type: disc;
}
.skip-to li:last-of-type {
list-style: none;
} }
.h-entry { .h-entry {
outline: 0.125rem solid var(--colour-primary-fg); background-color: var(--colour-primary-bg-accent);
outline-offset: 1rem; border: 0.125rem solid var(--colour-primary-fg);
border-radius: 0.25rem; border-radius: 1rem;
margin-inline: -1rem;
margin-block-start: var(--spacing-block-sm);
transition-duration: 250ms;
&:is(:hover, :focus, :focus-within, :focus-visible) {
background-color: var(--colour-primary-fg-accent);
color: var(--colour-primary-bg-accent);
transition-duration: 100ms;
}
+ .h-entry {
margin-block-start: var(--spacing-block-xs);
}
.u-url {
color: inherit;
display: inline-block;
height: 100%;
padding: 1rem;
text-decoration: none;
width: 100%;
}
.p-name {
margin-block-start: 0;
+ * {
margin-block-start: var(--spacing-block-xs);
}
}
p:has(.dt-published, .dt-updated) {
font-size: var(--font-size-sm);
font-style: italic;
&:not(.p-name + *) {
margin-block-start: 0;
}
}
}
.skip-to {
display: inline-block;
margin-block-start: var(--spacing-block-xs);
:is(ul, li) {
display: inline-block;
list-style-type: disc;
}
ul {
margin-block-start: var(--spacing-block-xs);
}
li:last-of-type {
list-style: none;
}
}
@media (prefers-reduced-motion: no-preference) {
.h-entry {
transition-property: background-color, color;
}
} }

View File

@@ -1,6 +1,7 @@
<?php <?php
namespace App\Controller; namespace App\Controller;
use App\Entity\BlogPost;
use App\Entity\Note; use App\Entity\Note;
use App\Form\Type\NoteType; use App\Form\Type\NoteType;
use DateTime; use DateTime;
@@ -14,7 +15,25 @@ use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Security\Http\Attribute\IsGranted;
use function Symfony\Component\HttpFoundation\Response; use function Symfony\Component\HttpFoundation\Response;
const MONTH_NAMES = [
'01' => 'January',
'02' => 'February',
'03' => 'March',
'04' => 'April',
'05' => 'May',
'06' => 'June',
'07' => 'July',
'08' => 'August',
'09' => 'September',
'10' => 'October',
'11' => 'November',
'12' => 'December',
];
class GuiController extends AbstractController { class GuiController extends AbstractController {
/**
* @return array<string,string>
*/
#[Route('/', name: 'index')] #[Route('/', name: 'index')]
#[Template('/index.html.twig')] #[Template('/index.html.twig')]
public function index(): array { public function index(): array {
@@ -23,7 +42,9 @@ class GuiController extends AbstractController {
'description' => 'Joe Carstairs\' personal website', 'description' => 'Joe Carstairs\' personal website',
]; ];
} }
/**
* @return array<string,mixed>
*/
#[Route('/notes', name: 'notes')] #[Route('/notes', name: 'notes')]
#[Template('/notes.html.twig')] #[Template('/notes.html.twig')]
public function notes( public function notes(
@@ -43,93 +64,115 @@ class GuiController extends AbstractController {
return [ return [
'title' => 'Joe Carstairs\' notes', 'title' => 'Joe Carstairs\' notes',
'description' => 'Joe Carstairs\' notes', 'description' => 'Joe Carstairs\' notes',
'isFeed' => True,
'notes' => $notesByYearAndMonth, 'notes' => $notesByYearAndMonth,
'years' => $years, 'years' => $years,
'months' => $monthsByYear, 'months' => $monthsByYear,
'monthNames' => [ 'monthNames' => MONTH_NAMES,
'01' => 'January', ];
'02' => 'February', }
'03' => 'March', /**
'04' => 'April', * @return array<string,mixed>
'05' => 'May', */
'06' => 'June', #[Route('/blog', name: 'blog_posts')]
'07' => 'July', #[Template('/blog_posts.html.twig')]
'08' => 'August', public function blogPosts(
'09' => 'September', EntityManagerInterface $entityManager,
'10' => 'October', ): array {
'11' => 'November', $posts = $entityManager->getRepository(BlogPost::class)->findAllOrderedBySlugDesc();
'12' => 'December', $years = $this->getYears($posts);
], $postsByYear = $this->groupByYear($posts);
$months = array_map(array: $years, callback: fn($year) => $this->getMonths($postsByYear[$year]));
$monthsByYear = array_combine(keys: $years, values: $months);
$groupByMonth = fn($posts) => $this->groupByMonth($posts);
$postsByYearAndMonth = array_map(
array: $postsByYear,
callback: $groupByMonth,
);
return [
'title' => 'Joe Carstairs\' blog',
'description' => 'Joe Carstairs\' blog',
'isFeed' => True,
'posts' => $postsByYearAndMonth,
'years' => $years,
'months' => $monthsByYear,
'monthNames' => MONTH_NAMES,
]; ];
} }
/** /**
* @param $notes Note[] * @@template T of \FeedEntry
* @return Note[][] * @param $entries T[]
* @return T[][]
*/ */
function groupByYear(array $notes): array { function groupByYear(array $entries): array {
$years = $this->getYears($notes); $years = $this->getYears($entries);
$filterByYear = fn(string $year) => $filterByYear = fn(string $year) =>
fn($note) => ($note->getPublishedDate()->format('Y') == $year); fn($entry) => ($entry->getPublishedDate()->format('Y') == $year);
$notesForYear = fn(string $year) => $entriesForYear = fn(string $year) =>
array_filter(array: $notes, callback: $filterByYear($year)); array_filter(array: $entries, callback: $filterByYear($year));
$notesByYear = array_map( $entriesByYear = array_map(
array: $years, array: $years,
callback: $notesForYear, callback: $entriesForYear,
); );
return array_combine(keys: $years, values: $notesByYear); return array_combine(keys: $years, values: $entriesByYear);
} }
/** /**
* @param $notes Note[] * @template T of \FeedEntry
* @return string[] * @param $entries T[]
*/ * @return string[]
function getYears(array $notes): array { */
$getYear = fn(Note $note): string => $note->getPublishedDate()->format('Y'); function getYears(array $entries): array {
$years = array_map(array: $notes, callback: $getYear); $getYear = fn($note): string => $note->getPublishedDate()->format('Y');
$years = array_map(array: $entries, callback: $getYear);
$years = array_unique($years); $years = array_unique($years);
arsort($years); arsort($years);
return $years; return $years;
} }
/** /**
* @param $notes Note[] * @template T of \FeedEntry
* @return Note[] * @param $entries T[]
* @return T[]
*/ */
function groupByMonth(array $notes): array { function groupByMonth(array $entries): array {
$months = $this->getMonths($notes); $months = $this->getMonths($entries);
$filterByMonth = fn(string $month) => $filterByMonth = fn(string $month) =>
fn($note) => ($note->getPublishedDate()->format('m') == $month); fn($entry) => ($entry->getPublishedDate()->format('m') == $month);
$notesByMonth = array_map( $entriesByMonth = array_map(
array: $months, array: $months,
callback: fn(string $month) => callback: fn(string $month) =>
array_filter(array: $notes, callback: $filterByMonth($month)), array_filter(array: $entries, callback: $filterByMonth($month)),
); );
return array_combine(keys: $months, values: $notesByMonth); return array_combine(keys: $months, values: $entriesByMonth);
} }
/** /**
* @param $notes Note[] * @@template T of \FeedEntry
* @return string[] * @param $entries T[]
* @return string[]
*/ */
function getMonths(array $notes): array { function getMonths(array $entries): array {
$getMonth = fn(Note $note): string => $note->getPublishedDate()->format('m'); $getMonth = fn($entry) => $entry->getPublishedDate()->format('m');
$months = array_map(array: $notes, callback: $getMonth); $months = array_map(array: $entries, callback: $getMonth);
$months = array_unique($months); $months = array_unique($months);
arsort($months); arsort($months);
return $months; return $months;
} }
/** /**
* @param $notes Note[] * @template T of \FeedEntry
* @return Note[] * @param $entries T[]
* @return T[]
*/ */
function sortBySlug(array $notes): array { function sortBySlug(array $entries): array {
$getSlug = fn(Note $note): string => $note->getSlug(); $getSlug = fn($entry): string => $entry->getSlug();
$slugs = array_map(array: $notes, callback: $getSlug); $slugs = array_map(array: $entries, callback: $getSlug);
$notesBySlug = array_combine(keys: $slugs, values: $notes); $entriesBySlug = array_combine(keys: $slugs, values: $entries);
krsort($notesBySlug); krsort($entriesBySlug);
return array_values($notesBySlug); return array_values($entriesBySlug);
} }
#[IsGranted('ROLE_EDITOR')] #[IsGranted('ROLE_EDITOR')]
@@ -185,8 +228,30 @@ class GuiController extends AbstractController {
return [ return [
'title' => 'Joe Carstairs\' notes', 'title' => 'Joe Carstairs\' notes',
'description' => 'Joe Carstairs\' notes', 'description' => 'Joe Carstairs\' notes',
'isFeedEntry' => True,
'note' => $note, 'note' => $note,
'slug' => $slug, 'slug' => $slug,
]; ];
} }
/**
* @return array<string,mixed>
*/
#[Route('/blog/{slug}', name: 'blog_post')]
#[Template('/blog_post.html.twig')]
public function blogPost(
EntityManagerInterface $entityManager,
string $slug,
): array {
$repository = $entityManager->getRepository(BlogPost::class);
$post = $repository->findOneBy(['slug' => $slug]);
return [
'title' => $post->getTitle(),
'description' => $post->getDescription(),
'isFeedEntry' => True,
'post' => $post,
'slug' => $slug,
];
}
} }

View File

@@ -2,12 +2,13 @@
namespace App\Entity; namespace App\Entity;
use App\Interface\FeedEntry;
use App\Repository\BlogPostRepository; use App\Repository\BlogPostRepository;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: BlogPostRepository::class)] #[ORM\Entity(repositoryClass: BlogPostRepository::class)]
class BlogPost class BlogPost implements FeedEntry
{ {
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue] #[ORM\GeneratedValue]

View File

@@ -2,12 +2,13 @@
namespace App\Entity; namespace App\Entity;
use App\Interface\FeedEntry;
use App\Repository\NoteRepository; use App\Repository\NoteRepository;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: NoteRepository::class)] #[ORM\Entity(repositoryClass: NoteRepository::class)]
class Note class Note implements FeedEntry
{ {
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue] #[ORM\GeneratedValue]

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Interface;
interface FeedEntry {
function getPublishedDate(): ?\DateTime;
function getSlug(): ?string;
}

View File

@@ -14,4 +14,15 @@ class BlogPostRepository extends ServiceEntityRepository
public function __construct(ManagerRegistry $registry) { public function __construct(ManagerRegistry $registry) {
parent::__construct($registry, BlogPost::class); parent::__construct($registry, BlogPost::class);
} }
/**
* @return BlogPost[]
*/
public function findAllOrderedBySlugDesc(): array
{
return $this->createQueryBuilder('n')
->orderBy('n.slug', 'DESC')
->getQuery()
->getResult();
}
} }

View File

@@ -12,8 +12,10 @@
<body> <body>
{{ include('components/_navbar.html.twig') }} {{ include('components/_navbar.html.twig') }}
<main> {% block main %}
{% block content %}{% endblock %} <main>
</main> {% block content %}{% endblock %}
</main>
{% endblock %}
</body> </body>
</html> </html>

View File

@@ -0,0 +1,47 @@
{% extends 'base.html.twig' %}
{% block content %}
{% if post %}
<article class="h-entry">
<aside>
<span>
This is a blog post by
<a class="p-author h-card" href="/">Joe Carstairs</a>.
</span>
<p>
Published: <time class="dt-published">{{ post.publishedDate.format('c') }}
</p>
{% if post.updatedDate %}}
<p>
Updated: <time class="dt-updated">{{ post.updatedDate.format('c') }}
</p>
{% endif %}
<span hidden>
<a class="u-url uid" href="{{ url('blog_post', { slug: post.slug }) }}">
Permalink
</a>
</span>
</aside>
<header>
<h1 class="p-name">{% apply markdown_to_html %}{{ post.title }}{% endapply %}</h1>
<p class="p-summary">
{% apply markdown_to_html %}{{ post.description }}{% endapply %}
</p>
</header>
{% apply markdown_to_html %}
<section class="e-content">
{{ post.content }}
</section>
{% endapply %}
</article>
{% else %}
<section>
<h1>Post not found</h1>
<p>I don't have a blog post '{{ slug }}'.</p>
<p>Go back to <a href="{{ path('blog_posts') }}">Blog</a>.</p>
</section>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,81 @@
{% extends 'base.html.twig' %}
{% block content %}
<section class="h-feed">
<h1 class="p-name">Joe Carstairs' blog</h1>
<p hidden>
This blog is written by
<a class="p-author h-card" href="{{ url('index') }}">
Joe Carstairs
</a>.
</p>
<p hidden>
<a class="u-url" href="{{ url('blog_posts') }}">Permalink</a>
</p>
{% if years %}
<nav class="skip-to">
Skip to:
<ul>
{% for year in years %}
<li><a href="#{{ year }}">{{ year }}</a></li>
{% endfor %}
</ul>
</nav>
{% endif %}
{% for year in years %}
<h2 id="{{ year }}">{{ year }}</h2>
<nav class="skip-to">
Skip to:
<ul>
{% for month in months[year] %}
<li><a href="#{{ year }}-{{ month }}">
{{ monthNames[month] }} <span class="visually-hidden">{{ year }}</span>
</a></li>
{% endfor %}
</ul>
</nav>
{% for month in months[year] %}
<h3 id="{{ year }}-{{ month }}">{{ monthNames[month] }}</h3>
{% for post in posts[year][month] %}
<section class="h-entry">
<a class="u-url" href="{{ path('blog_post', { 'slug': post.slug }) }}">
<h4 class="p-name">
{{ post.title }}
</h4>
<p>
Added:
<time class="dt-published" datetime="{{ post.publishedDate.format('c') }}">
{{ post.publishedDate.format('j F Y') }}
</time>
</p>
{% if post.updatedDate %}
<p>
Updated:
<time class="dt-updated" datetime="{{ post.updatedDate.format('c') }}">
{{ post.updatedDate.format('j F Y') }}
</time>
</p>
{% endif %}
<section class="p-summary">
{% apply markdown_to_html %}
{{ post.description|markdown_to_html|striptags('<i><em><b><strong><sup><p>')|html_to_markdown }}
{% endapply %}
</section>
</a>
</section>
{% endfor %}
{% endfor %}
{% else %}
<p>I have no blog posts.</p>
{% endfor %}
</section>
{% endblock %}

View File

@@ -4,7 +4,7 @@
<a href="/">Home</a> <a href="/">Home</a>
</li> </li>
<li> <li>
<a href="/blog">Blog</a> <a href="{{ path('blog_posts') }}">Blog</a>
</li> </li>
<li> <li>
<a href="{{ path('notes') }}">Notes</a> <a href="{{ path('notes') }}">Notes</a>

View File

@@ -1,4 +1,9 @@
<link rel="stylesheet" href="/css/reset.css" /> <link rel="stylesheet" href="/css/reset.css" />
<link rel="stylesheet" href="/css/base.css" /> <link rel="stylesheet" href="/css/base.css" />
<link rel="stylesheet" href="/css/hcard.css" /> <link rel="stylesheet" href="/css/hcard.css" />
<link rel="stylesheet" href="/css/feed.css" /> {% if isFeed|default(false) %}
<link rel="stylesheet" href="/css/feed.css" />
{% endif %}
{% if isFeedEntry|default(false) %}
<link rel="stylesheet" href="/css/feed-entry.css" />
{% endif %}

View File

@@ -1,23 +1,28 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block content %} {% block main %}
{% if note %} {% if note %}
<section class="h-entry"> <article class="h-entry">
<h1 class="p-name">Note {{ note.slug }}</h1> <header>
<a hidden class="p-author h-card" href="{{ url('index') }}">Joe Carstairs</a> <h1 class="p-name">Note {{ note.slug }}</h1>
<a hidden class="u-url" href="{{ url('note', { slug: note.slug }) }}">Permalink</a> <p hidden><a class="p-author h-card" href="{{ url('index') }}">Joe Carstairs</a></p>
<time class="dt-published">{{ note.publishedDate.format('c') }} <p hidden <a class="u-url" href="{{ url('note', { slug: note.slug }) }}">Permalink</a></p>
{% apply markdown_to_html %} <p>Added: <time class="dt-published">{{ note.publishedDate.format('c') }}</time></p>
<section class="e-content"> </header>
{{ note.content }}
</section> <section class="e-content">
{% endapply %} {{ note.content|markdown_to_html }}
</section> </section>
</article>
{% else %} {% else %}
<section> <main>
<h1>Note not found</h1> <header>
<p>I don't have a note '{{ slug }}'.</p> <h1>Note not found</h1>
<p>Go back to <a href="/notes">Notes</a>.</p> </header>
</section> <section>
<p>I don't have a note '{{ slug }}'.</p>
<p>Go back to <a href="/notes">Notes</a>.</p>
</section>
</main>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@@ -14,14 +14,16 @@
<a class="u-url" href="{{ url('notes') }}">Permalink</a> <a class="u-url" href="{{ url('notes') }}">Permalink</a>
</p> </p>
<nav class="skip-to"> {% if years %}
Skip to: <nav class="skip-to">
<ul> Skip to:
{% for year in years %} <ul>
<li><a href="#{{ year }}">{{ year }}</a></li> {% for year in years %}
{% endfor %} <li><a href="#{{ year }}">{{ year }}</a></li>
</ul> {% endfor %}
</nav> </ul>
</nav>
{% endif %}
{% for year in years %} {% for year in years %}
<h2 id="{{ year }}">{{ year }}</h2> <h2 id="{{ year }}">{{ year }}</h2>
@@ -42,18 +44,23 @@
{% for note in notes[year][month] %} {% for note in notes[year][month] %}
<section class="h-entry"> <section class="h-entry">
<h4><a class="p-name u-url" href="{{ url('note', { slug: note.slug }) }}"> <a class="u-url" href="{{ url('note', { slug: note.slug }) }}">
Note {{ note.slug }} <h4 class="p-name">
</a></h4> Note {{ note.slug }}
<time class="dt-published" datetime="{{ note.publishedDate.format('c') }}"> </h4>
{{ note.publishedDate.format('j F Y') }} <p>
</time> Added:
<time class="dt-published" datetime="{{ note.publishedDate.format('c') }}">
{{ note.publishedDate.format('j F Y') }}
</time>
</p>
<section class="e-content"> <section class="e-content">
{% apply markdown_to_html %} {% apply markdown_to_html %}
{{ note.content }} {{ note.content|markdown_to_html|striptags('<i><em><b><strong><sup><p>')|html_to_markdown }}
{% endapply %} {% endapply %}
</section> </section>
</a>
</section> </section>
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}

View File

@@ -1,5 +1,3 @@
Add blog post page
Add blog index page
Add a most recent blog post to my home page Add a most recent blog post to my home page
Add a most recent note to my home page Add a most recent note to my home page
Write a script to take a database dump Write a script to take a database dump