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/orm": "^3.3",
"league/commonmark": "^2.7",
"league/html-to-markdown": "^5.1",
"phpdocumentor/reflection-docblock": "^5.6",
"phpstan/phpdoc-parser": "^2.1",
"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",
"This file is @generated automatically"
],
"content-hash": "c3cbc37c53092d5fb23e7a7e21304c25",
"content-hash": "194a6d80a9896ce73994dc1b6c059f1d",
"packages": [
{
"name": "composer/semver",
@@ -1635,6 +1635,95 @@
],
"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",
"version": "3.9.0",

View File

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

View File

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

View File

@@ -27,7 +27,7 @@
--colour-primary-fg: var(--colour-primary-90);
--colour-primary-fg-accent: var(--colour-primary-80);
--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-bg: var(--colour-primary-15);
--colour-hyperlink: var(--colour-hyperlink-80);
@@ -56,7 +56,7 @@
--colour-primary-fg: var(--colour-primary-20);
--colour-primary-fg-accent: var(--colour-primary-40);
--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);
}
}
@@ -162,13 +162,13 @@ h3, h4, h5, h6 {
/** Hyperlinks */
a:is(:link, :visited) {
:is(:link, :visited) {
color: var(--colour-hyperlink);
text-decoration: underline;
display: inline;
}
a:hover {
:hover {
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 {
font-style: italic;
font-size: var(--font-size-sm);

View File

@@ -1,23 +1,76 @@
.skip-to {
display: inline-block;
margin-block-start: var(--spacing-block-xs);
h2 {
margin-block-start: var(--spacing-block-md);
}
.skip-to ul {
margin-block-start: var(--spacing-block-xs);
}
.skip-to :is(ul, li) {
display: inline-block;
list-style-type: disc;
}
.skip-to li:last-of-type {
list-style: none;
h3 {
margin-block-start: var(--spacing-block-sm);
}
.h-entry {
outline: 0.125rem solid var(--colour-primary-fg);
outline-offset: 1rem;
border-radius: 0.25rem;
background-color: var(--colour-primary-bg-accent);
border: 0.125rem solid var(--colour-primary-fg);
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
namespace App\Controller;
use App\Entity\BlogPost;
use App\Entity\Note;
use App\Form\Type\NoteType;
use DateTime;
@@ -14,7 +15,25 @@ use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
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 {
/**
* @return array<string,string>
*/
#[Route('/', name: 'index')]
#[Template('/index.html.twig')]
public function index(): array {
@@ -23,7 +42,9 @@ class GuiController extends AbstractController {
'description' => 'Joe Carstairs\' personal website',
];
}
/**
* @return array<string,mixed>
*/
#[Route('/notes', name: 'notes')]
#[Template('/notes.html.twig')]
public function notes(
@@ -43,93 +64,115 @@ class GuiController extends AbstractController {
return [
'title' => 'Joe Carstairs\' notes',
'description' => 'Joe Carstairs\' notes',
'isFeed' => True,
'notes' => $notesByYearAndMonth,
'years' => $years,
'months' => $monthsByYear,
'monthNames' => [
'01' => 'January',
'02' => 'February',
'03' => 'March',
'04' => 'April',
'05' => 'May',
'06' => 'June',
'07' => 'July',
'08' => 'August',
'09' => 'September',
'10' => 'October',
'11' => 'November',
'12' => 'December',
],
'monthNames' => MONTH_NAMES,
];
}
/**
* @return array<string,mixed>
*/
#[Route('/blog', name: 'blog_posts')]
#[Template('/blog_posts.html.twig')]
public function blogPosts(
EntityManagerInterface $entityManager,
): array {
$posts = $entityManager->getRepository(BlogPost::class)->findAllOrderedBySlugDesc();
$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[]
* @return Note[][]
* @@template T of \FeedEntry
* @param $entries T[]
* @return T[][]
*/
function groupByYear(array $notes): array {
$years = $this->getYears($notes);
function groupByYear(array $entries): array {
$years = $this->getYears($entries);
$filterByYear = fn(string $year) =>
fn($note) => ($note->getPublishedDate()->format('Y') == $year);
$notesForYear = fn(string $year) =>
array_filter(array: $notes, callback: $filterByYear($year));
$notesByYear = array_map(
fn($entry) => ($entry->getPublishedDate()->format('Y') == $year);
$entriesForYear = fn(string $year) =>
array_filter(array: $entries, callback: $filterByYear($year));
$entriesByYear = array_map(
array: $years,
callback: $notesForYear,
callback: $entriesForYear,
);
return array_combine(keys: $years, values: $notesByYear);
return array_combine(keys: $years, values: $entriesByYear);
}
/**
* @param $notes Note[]
* @return string[]
*/
function getYears(array $notes): array {
$getYear = fn(Note $note): string => $note->getPublishedDate()->format('Y');
$years = array_map(array: $notes, callback: $getYear);
* @template T of \FeedEntry
* @param $entries T[]
* @return string[]
*/
function getYears(array $entries): array {
$getYear = fn($note): string => $note->getPublishedDate()->format('Y');
$years = array_map(array: $entries, callback: $getYear);
$years = array_unique($years);
arsort($years);
return $years;
}
/**
* @param $notes Note[]
* @return Note[]
* @template T of \FeedEntry
* @param $entries T[]
* @return T[]
*/
function groupByMonth(array $notes): array {
$months = $this->getMonths($notes);
function groupByMonth(array $entries): array {
$months = $this->getMonths($entries);
$filterByMonth = fn(string $month) =>
fn($note) => ($note->getPublishedDate()->format('m') == $month);
$notesByMonth = array_map(
fn($entry) => ($entry->getPublishedDate()->format('m') == $month);
$entriesByMonth = array_map(
array: $months,
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[]
* @return string[]
* @@template T of \FeedEntry
* @param $entries T[]
* @return string[]
*/
function getMonths(array $notes): array {
$getMonth = fn(Note $note): string => $note->getPublishedDate()->format('m');
$months = array_map(array: $notes, callback: $getMonth);
function getMonths(array $entries): array {
$getMonth = fn($entry) => $entry->getPublishedDate()->format('m');
$months = array_map(array: $entries, callback: $getMonth);
$months = array_unique($months);
arsort($months);
return $months;
}
/**
* @param $notes Note[]
* @return Note[]
* @template T of \FeedEntry
* @param $entries T[]
* @return T[]
*/
function sortBySlug(array $notes): array {
$getSlug = fn(Note $note): string => $note->getSlug();
$slugs = array_map(array: $notes, callback: $getSlug);
$notesBySlug = array_combine(keys: $slugs, values: $notes);
krsort($notesBySlug);
return array_values($notesBySlug);
function sortBySlug(array $entries): array {
$getSlug = fn($entry): string => $entry->getSlug();
$slugs = array_map(array: $entries, callback: $getSlug);
$entriesBySlug = array_combine(keys: $slugs, values: $entries);
krsort($entriesBySlug);
return array_values($entriesBySlug);
}
#[IsGranted('ROLE_EDITOR')]
@@ -185,8 +228,30 @@ class GuiController extends AbstractController {
return [
'title' => 'Joe Carstairs\' notes',
'description' => 'Joe Carstairs\' notes',
'isFeedEntry' => True,
'note' => $note,
'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;
use App\Interface\FeedEntry;
use App\Repository\BlogPostRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: BlogPostRepository::class)]
class BlogPost
class BlogPost implements FeedEntry
{
#[ORM\Id]
#[ORM\GeneratedValue]

View File

@@ -2,12 +2,13 @@
namespace App\Entity;
use App\Interface\FeedEntry;
use App\Repository\NoteRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: NoteRepository::class)]
class Note
class Note implements FeedEntry
{
#[ORM\Id]
#[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) {
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>
{{ include('components/_navbar.html.twig') }}
<main>
{% block content %}{% endblock %}
</main>
{% block main %}
<main>
{% block content %}{% endblock %}
</main>
{% endblock %}
</body>
</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>
</li>
<li>
<a href="/blog">Blog</a>
<a href="{{ path('blog_posts') }}">Blog</a>
</li>
<li>
<a href="{{ path('notes') }}">Notes</a>

View File

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

View File

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