Update to Eleventy v3 #11

Merged
wonderfulfrog merged 56 commits from feat/11ty-v3 into main 2025-01-27 18:23:38 -08:00
282 changed files with 4577 additions and 5016 deletions

View file

@ -1,15 +0,0 @@
module.exports = {
env: {
node: true,
es6: true,
},
extends: ["eslint:recommended", "plugin:prettier/recommended"],
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
ignorePatterns: ["!.eleventy.js"],
rules: {
"prettier/prettier": "error",
},
};

2
.nvmrc
View file

@ -1 +1 @@
v20.11.1
v22.11.0

View file

@ -1 +0,0 @@
dist

View file

@ -1,4 +1,6 @@
const postsByTag = (collection) => {
import dayjs from "dayjs";
export const postsByTag = (collection) => {
const posts = collection.getFilteredByTag("post");
const postsByTag = {};
@ -13,7 +15,22 @@ const postsByTag = (collection) => {
return postsByTag;
};
const catalogueByType = (collection) => {
export const collectionByTag = (collection, collectionName) => {
const items = collection.getFilteredByTag(collectionName);
const itemsByTag = {};
for (const item of items) {
for (const tag of item.data.tags) {
itemsByTag[tag] ??= [];
itemsByTag[tag].push(item);
}
}
return itemsByTag;
};
export const catalogueByType = (collection) => {
const allItems = collection.getFilteredByTag("catalogue");
const catalogueByType = {};
@ -28,8 +45,3 @@ const catalogueByType = (collection) => {
return catalogueByType;
};
module.exports = {
catalogueByType,
postsByTag,
};

View file

@ -1,9 +1,7 @@
const dir = {
export const dir = {
assets: "assets",
data: "_data",
includes: "_includes",
input: "src",
output: "dist",
};
module.exports = { dir };

View file

@ -0,0 +1,22 @@
export default {
light: {
primary: "188deg 84% 35%",
secondary: "8deg 84% 50%",
background: "0deg 0% 98%",
surface: "188deg 27% 94%",
border: "188deg 48% 80%",
text: "0deg 0% 4%",
fadeText: "188deg 12% 32%",
shadow: "188deg 100% 18%",
},
dark: {
primary: "188deg 100% 30%",
secondary: "8deg 84% 43%",
background: "0deg 0% 9%",
surface: "202deg 13% 14%",
border: "208deg 27% 15%",
text: "0deg 0% 98%",
fadeText: "188deg 12% 70%",
shadow: "188deg 100% 18%",
},
};

View file

@ -1,22 +0,0 @@
{
"light": {
"primary": "188deg 84% 35%",
"secondary": "8deg 84% 50%",
"background": "0deg 0% 98%",
"surface": "188deg 27% 94%",
"border": "188deg 48% 80%",
"text": "0deg 0% 4%",
"fadeText": "188deg 12% 32%",
"shadow": "188deg 100% 18%"
},
"dark": {
"primary": "188deg 100% 30%",
"secondary": "8deg 84% 43%",
"background": "0deg 0% 9%",
"surface": "202deg 13% 14%",
"border": "208deg 27% 15%",
"text": "0deg 0% 98%",
"fadeText": "188deg 12% 70%",
"shadow": "188deg 100% 18%"
}
}

View file

@ -0,0 +1,60 @@
export default {
display: {
family: "Anek Latin",
format: "truetype",
weights: {
ExtraBold: {
path: "/aneklatin/AnekLatin-ExtraBold.ttf",
"font-style": "normal",
weight: 800,
},
Bold: {
path: "/aneklatin/AnekLatin-Bold.ttf",
"font-style": "normal",
weight: 700,
},
},
},
body: {
family: "iA Writer Quattro V",
format: "woff2",
weights: {
Regular: {
path: "/quattro/iAWriterQuattroS-Regular.woff2",
"font-style": "normal",
weight: 400,
},
Italic: {
path: "/quattro/iAWriterQuattroS-Italic.woff2",
"font-style": "italic",
weight: 400,
},
Bold: {
path: "/quattro/iAWriterQuattroS-Bold.woff2",
"font-style": "normal",
weight: 650,
},
BoldItalic: {
path: "/quattro/iAWriterQuattroS-BoldItalic.woff2",
"font-style": "italic",
weight: 650,
},
},
},
monospace: {
family: "IBM Plex Mono",
format: "truetype",
weights: {
Regular: {
path: "/ibmplexmono/IBMPlexMono-Regular.ttf",
"font-style": "normal",
weight: 400,
},
Italic: {
path: "/ibmplexmono/IBMPlexMono-Italic.ttf",
"font-style": "italic",
weight: 400,
},
},
},
};

View file

@ -1,60 +0,0 @@
{
"display": {
"family": "Anek Latin",
"format": "truetype",
"weights": {
"ExtraBold": {
"path": "/aneklatin/AnekLatin-ExtraBold.ttf",
"font-style": "normal",
"weight": 800
},
"Bold": {
"path": "/aneklatin/AnekLatin-Bold.ttf",
"font-style": "normal",
"weight": 700
}
}
},
"body": {
"family": "iA Writer Quattro V",
"format": "woff2",
"weights": {
"Regular": {
"path": "/quattro/iAWriterQuattroS-Regular.woff2",
"font-style": "normal",
"weight": 400
},
"Italic": {
"path": "/quattro/iAWriterQuattroS-Italic.woff2",
"font-style": "italic",
"weight": 400
},
"Bold": {
"path": "/quattro/iAWriterQuattroS-Bold.woff2",
"font-style": "normal",
"weight": 650
},
"BoldItalic": {
"path": "/quattro/iAWriterQuattroS-BoldItalic.woff2",
"font-style": "italic",
"weight": 650
}
}
},
"monospace": {
"family": "IBM Plex Mono",
"format": "truetype",
"weights": {
"Regular": {
"path": "/ibmplexmono/IBMPlexMono-Regular.ttf",
"font-style": "normal",
"weight": 400
},
"Italic": {
"path": "/ibmplexmono/IBMPlexMono-Italic.ttf",
"font-style": "italic",
"weight": 400
}
}
}
}

View file

@ -0,0 +1,11 @@
export default {
0: 0,
0.25: 4,
0.5: 8,
1: 16,
1.5: 24,
2: 32,
3: 48,
4: 64,
5: 80,
};

View file

@ -1,11 +0,0 @@
{
"0": 0,
"0.25": 4,
"0.5": 8,
"1": 16,
"1.5": 24,
"2": 32,
"3": 48,
"4": 64,
"5": 80
}

View file

@ -1,19 +1,19 @@
const dayjs = require("dayjs");
const utc = require("dayjs/plugin/utc");
const advancedFormat = require("dayjs/plugin/advancedFormat");
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc.js";
import advancedFormat from "dayjs/plugin/advancedFormat.js";
const pluralizeBase = require("pluralize");
import pluralizeBase from "pluralize";
const keys = Object.keys;
const values = Object.values;
const entries = Object.entries;
export const keys = Object.keys;
export const values = Object.values;
export const entries = Object.entries;
dayjs.extend(utc);
dayjs.extend(advancedFormat);
const formatDate = (date, format) => dayjs.utc(date).format(format);
export const formatDate = (date, format) => dayjs.utc(date).format(format);
const organizeByDate = (collection) => {
export const organizeByDate = (collection) => {
const collectionByDate = {};
collection.forEach((item) => {
@ -29,15 +29,23 @@ const organizeByDate = (collection) => {
return collectionByDate;
};
const allTags = (collection, ignore = []) => {
const tagSet = new Set(collection.flatMap((item) => item.data.tags));
export const transformByDate = (collection) => {
const collectionByDate = {};
ignore.forEach((tag) => tagSet.delete(tag));
collection.forEach((item) => {
const year = formatDate(item.date, "YYYY");
return [...tagSet];
if (!collectionByDate[year]) {
return (collectionByDate[year] = { value: year, data: [item] });
}
collectionByDate[year].data.push(item);
});
return collectionByDate;
};
const allTagCounts = (collection, ignore = ["post"]) => {
export const allTagCounts = (collection, ignore = ["post"]) => {
if (!collection.length) {
throw new Error("Invalid collection, no items");
}
@ -62,28 +70,23 @@ const allTagCounts = (collection, ignore = ["post"]) => {
return tagArray;
};
const filter = (collection, filters = []) => {
export const filter = (collection, filters = []) => {
return collection.filter((item) => !filters.includes(item));
};
const pluralize = (string, count = 0) => {
export const pluralize = (string, count = 0) => {
return pluralizeBase(string, count);
};
const filterCatalogueTags = (tags) => {
// In the case of catalogue items, the 0-index is "catalogue"
// and the 1-index is the catalogueType. We don't need to
// show those in the front-end.
return filter(tags, [tags[0], tags[1]]);
export const limit = (collection, limit = 5) => collection.slice(0, limit);
export const filterFavourites = (collection) => {
return collection.filter(
(item) => item.data.favourite || item.data.isFavourite,
);
};
const limit = (collection, limit = 5) => collection.slice(0, limit);
const filterFavourites = (collection) => {
return collection.filter((item) => item.data.favourite);
};
const isOld = (dateArg) => {
export const isOld = (dateArg) => {
const date = dayjs(dateArg);
const now = dayjs();
@ -91,19 +94,3 @@ const isOld = (dateArg) => {
return diffInYears >= 2;
};
module.exports = {
allTagCounts,
allTags,
entries,
filter,
filterCatalogueTags,
filterFavourites,
formatDate,
isOld,
keys,
limit,
organizeByDate,
pluralize,
values,
};

View file

@ -1,8 +1,10 @@
const markdownIt = require("markdown-it");
const markdownItFootnote = require("markdown-it-footnote");
const markdownItPrism = require("markdown-it-prism");
const markdownItAbbr = require("markdown-it-abbr");
const markdownItAnchor = require("markdown-it-anchor");
import markdownIt from "markdown-it";
import markdownItFootnote from "markdown-it-footnote";
import markdownItPrism from "markdown-it-prism";
import markdownItAbbr from "markdown-it-abbr";
import markdownItAnchor from "markdown-it-anchor";
import markdownItImplicitFigures from "markdown-it-image-figures";
import markdownItAttrs from "markdown-it-attrs";
const markdown = markdownIt({
html: true,
@ -13,9 +15,13 @@ const markdown = markdownIt({
.use(markdownItFootnote)
.use(markdownItAbbr)
.use(markdownItAnchor)
.use(markdownItImplicitFigures, {
figcaption: true,
})
.use(markdownItPrism, {
defaultLanguage: "plaintext",
});
})
.use(markdownItAttrs);
markdown.renderer.rules.footnote_block_open = (_tokens, _idx, options) => {
return (
@ -27,4 +33,4 @@ markdown.renderer.rules.footnote_block_open = (_tokens, _idx, options) => {
);
};
module.exports = markdown;
export default markdown;

View file

@ -1,76 +0,0 @@
const Image = require("@11ty/eleventy-img");
const stringifyAttributes = (attributeMap) => {
return Object.entries(attributeMap)
.map(([attribute, value]) => {
if (typeof value === "undefined") return "";
return `${attribute}="${value}"`;
})
.join(" ");
};
const imageShortcode = async (
src,
alt = "",
caption = "",
className = undefined,
placeholder = "",
widths = [400, 800, 1280],
formats = ["webp", "jpeg"],
sizes = "100vw",
) => {
if (!src) {
return `<div class="image-placeholder">${placeholder}</div>`;
}
const metadata = await Image(src, {
widths: [...widths, null],
formats: [...formats, null],
outputDir: "dist/assets/images",
urlPath: "/assets/images",
sharpOptions: {
animated: true,
},
cacheOptions: {
duration: "7d",
},
});
const lowsrc = metadata.jpeg[metadata.jpeg.length - 1];
const imageSources = Object.values(metadata)
.map((imageFormat) => {
return ` <source type="${imageFormat[0].sourceType}" srcset="${imageFormat
.map((entry) => entry.srcset)
.join(", ")}" sizes="${sizes}">`;
})
.join("\n");
const imageAttributes = stringifyAttributes({
src: lowsrc.url,
width: lowsrc.width,
height: lowsrc.height,
alt,
loading: "lazy",
decoding: "async",
});
const imageElement = caption
? `<figure class="[ flow flex-col items-center justify-center ${className ? ` ${className} ` : ""}]">
<picture class="flex items-center justify-center">
${imageSources}
<img
${imageAttributes}>
</picture>
<figcaption>${caption}</figcaption>
</figure>`
: `<picture class="[ flex-col items-center justify-center ${className ? ` ${className} ` : ""}]">
${imageSources}
<img
${imageAttributes}>
</picture>`;
return imageElement;
};
module.exports = imageShortcode;

View file

@ -12,4 +12,4 @@ const liteYoutube = (id, label) => {
</div>`;
};
module.exports = liteYoutube;
export default liteYoutube;

View file

@ -1,6 +1,6 @@
const htmlmin = require("html-minifier-terser");
import htmlmin from "html-minifier-terser";
module.exports = (eleventyConfig) => {
export default function (eleventyConfig) {
eleventyConfig.addTransform("html-minify", (content, path) => {
if (path && path.endsWith(".html")) {
return htmlmin.minify(content, {
@ -17,4 +17,4 @@ module.exports = (eleventyConfig) => {
}
return content;
});
};
}

View file

@ -1,16 +1,15 @@
const pluginRss = require("@11ty/eleventy-plugin-rss");
const pluginNoRobots = require("eleventy-plugin-no-robots");
import util from "util";
import pluginRss from "@11ty/eleventy-plugin-rss";
import pluginNoRobots from "eleventy-plugin-no-robots";
import { eleventyImageTransformPlugin } from "@11ty/eleventy-img";
const {
catalogueByType,
postsByTag,
} = require("./config/collections/index.js");
const { dir } = require("./config/constants.js");
const {
import { collectionByTag, postsByTag } from "./config/collections/index.js";
import { dir } from "./config/constants.js";
import {
allTagCounts,
entries,
filter,
filterCatalogueTags,
filterFavourites,
formatDate,
isOld,
@ -19,12 +18,13 @@ const {
organizeByDate,
pluralize,
values,
} = require("./config/filters/index.js");
const markdown = require("./config/plugins/markdown.js");
const imageShortcode = require("./config/shortcodes/image.js");
const liteYoutube = require("./config/shortcodes/youtube.js");
} from "./config/filters/index.js";
import markdown from "./config/plugins/markdown.js";
import liteYoutube from "./config/shortcodes/youtube.js";
module.exports = (eleventyConfig) => {
import htmlConfigTransform from "./config/transforms/html-config.js";
export default function (eleventyConfig) {
eleventyConfig.addWatchTarget("./src/css");
// --------------------- Plugins ---------------------
@ -32,14 +32,24 @@ module.exports = (eleventyConfig) => {
eleventyConfig.addPlugin(pluginNoRobots);
// --------------------- Custom Collections -----------------------
eleventyConfig.addCollection("catalogueByType", catalogueByType);
eleventyConfig.addCollection("postsByTag", postsByTag);
eleventyConfig.addCollection("booksByTag", (collection) =>
collectionByTag(collection, "book"),
);
eleventyConfig.addCollection("gamesByTag", (collection) =>
collectionByTag(collection, "game"),
);
eleventyConfig.addCollection("showsByTag", (collection) =>
collectionByTag(collection, "tv"),
);
eleventyConfig.addCollection("moviesByTag", (collection) =>
collectionByTag(collection, "movie"),
);
// --------------------- Custom Filters -----------------------
eleventyConfig.addFilter("allTagCounts", allTagCounts);
eleventyConfig.addFilter("entries", entries);
eleventyConfig.addFilter("filter", filter);
eleventyConfig.addFilter("filterCatalogueTags", filterCatalogueTags);
eleventyConfig.addFilter("filterFavourites", filterFavourites);
eleventyConfig.addFilter("formatDate", formatDate);
eleventyConfig.addFilter("isOld", isOld);
@ -50,7 +60,26 @@ module.exports = (eleventyConfig) => {
eleventyConfig.addFilter("pluralize", pluralize);
// --------------------- Custom Transforms -----------------------
eleventyConfig.addPlugin(require("./config/transforms/html-config.js"));
eleventyConfig.addPlugin(htmlConfigTransform);
// Image Transforms
// Works with any <img> tag in output files.
eleventyConfig.addPlugin(eleventyImageTransformPlugin, {
extensions: "html",
formats: ["webp", "jpeg"],
sharpOptions: {
animated: true,
},
defaultAttributes: {
loading: "lazy",
decoding: "async",
},
failOnError: false,
});
// --------------------- Passthrough File Copy -----------------------
["src/assets/fonts/", "src/assets/images"].forEach((path) =>
@ -62,9 +91,12 @@ module.exports = (eleventyConfig) => {
eleventyConfig.setLibrary("md", markdown);
// --------------------- Shortcodes -----------------------
eleventyConfig.addShortcode("image", imageShortcode);
eleventyConfig.addShortcode("youtube", liteYoutube);
eleventyConfig.addFilter("console", function (value) {
return util.inspect(value);
});
return {
// Optional (default is set): If your site deploys to a subdirectory, change `pathPrefix`, for example with with GitHub pages
pathPrefix: "/",
@ -75,4 +107,4 @@ module.exports = (eleventyConfig) => {
dir,
};
};
}

5868
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,38 +5,34 @@
"repository": "https://github.com/wonderfulfrog/wonderfulfrog.com",
"author": "Devin Haska",
"license": "MIT",
"type": "module",
"scripts": {
"debug": "DEBUG=Eleventy* eleventy --serve --watch",
"dev": "eleventy --serve --watch --quiet",
"lint": "eslint src/ config/",
"build": "eleventy"
"debug": "DEBUG=Eleventy* npx @11ty/eleventy",
"start": "npx @11ty/eleventy --serve --quiet",
"build": "npx @11ty/eleventy"
},
"devDependencies": {
"@11ty/eleventy": "^2.0.1",
"@11ty/eleventy": "^3.0.0",
"@11ty/eleventy-fetch": "^4.0.0",
"@11ty/eleventy-img": "^3.1.8",
"@11ty/eleventy-img": "^5.0.0",
"@11ty/eleventy-plugin-rss": "^1.2.0",
"@netlify/functions": "^2.6.0",
"autoprefixer": "^10.4.17",
"cheerio": "^1.0.0-rc.12",
"cssnano": "^6.0.3",
"dayjs": "^1.11.10",
"dotenv": "^16.4.5",
"eleventy-plugin-no-robots": "^1.0.1",
"eslint": "8.56.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-prettier": "5.1.3",
"html-minifier-terser": "^7.2.0",
"markdown-it": "^14.0.0",
"markdown-it-abbr": "^2.0.0",
"markdown-it-anchor": "^8.6.7",
"markdown-it-attrs": "^4.3.1",
"markdown-it-footnote": "^4.0.0",
"markdown-it-image-figures": "^2.1.1",
"markdown-it-prism": "^2.3.0",
"netlify-plugin-cache": "^1.0.3",
"pluralize": "^8.0.0",
"postcss": "^8.4.33",
"postcss-import": "^16.0.0",
"postcss-import-ext-glob": "^2.1.1",
"prettier": "3.2.4"
"postcss-import-ext-glob": "^2.1.1"
}
}

View file

@ -87,4 +87,4 @@ const blogroll = [
const sortedBlogroll = blogroll.sort((a, b) => a.title.localeCompare(b.title));
module.exports = sortedBlogroll;
export default sortedBlogroll;

View file

@ -1,98 +0,0 @@
require("dotenv").config();
const EleventyFetch = require("@11ty/eleventy-fetch");
const dayjs = require("dayjs");
const utc = require("dayjs/plugin/utc");
const relativeTime = require("dayjs/plugin/relativeTime");
dayjs.extend(utc);
dayjs.extend(relativeTime);
const lastFmApiKey = process.env.LAST_FM_API_KEY;
const baseUrl = "http://ws.audioscrobbler.com";
const username = "wonderfulfrog";
const fetchLastFm = async (method, duration, extraArgs) => {
try {
const path = `/2.0/?method=${method}&user=${username}&api_key=${lastFmApiKey}&format=json`;
let url = `${baseUrl}${path}`;
if (extraArgs) {
url = `${url}&${extraArgs}`;
}
const response = await EleventyFetch(url, { duration, type: "json" });
return response;
} catch (e) {
console.error(`Error fetching last.fm data for method=${method}`, e);
return undefined;
}
};
const fetchRecentAlbums = async (period = "7day") => {
const response = await fetchLastFm(
"user.gettopalbums",
"7d",
`period=${period}`,
);
if (!response) {
return [];
}
const albums = response.topalbums.album.slice(0, 8);
const recentAlbums = albums.map((album) => {
const extraLargeImage = album.image.find(
(img) => img.size === "extralarge",
);
const imageUrl = extraLargeImage ? extraLargeImage["#text"] : "";
return {
artist: album.artist.name,
artistMbid: album.artist.mbid,
album: album.name,
albumMbid: album.mbid,
playcount: album.playcount,
url: album.url,
imageUrl,
};
});
return recentAlbums;
};
const fetchRecentTracks = async () => {
const response = await fetchLastFm("user.getrecenttracks", "5m");
if (!response) {
return [];
}
const tracks = response.recenttracks.track.slice(0, 5);
const recentTracks = tracks.map((track) => {
const timestamp = track.date
? dayjs(track.date["#text"]).utc(true).fromNow()
: dayjs().fromNow();
return {
artist: track.artist["#text"],
track: track.name,
url: track.url,
timestamp,
};
});
return recentTracks;
};
module.exports = async function () {
const recentTracks = await fetchRecentTracks();
const recentAlbums = await fetchRecentAlbums();
return {
recentTracks,
recentAlbums,
};
};

View file

@ -1,10 +1,10 @@
require("dotenv").config();
import "dotenv/config";
const EleventyFetch = require("@11ty/eleventy-fetch");
const cheerio = require("cheerio");
const dayjs = require("dayjs");
const utc = require("dayjs/plugin/utc");
const relativeTime = require("dayjs/plugin/relativeTime");
import EleventyFetch from "@11ty/eleventy-fetch";
import cheerio from "cheerio";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc.js";
import relativeTime from "dayjs/plugin/relativeTime.js";
dayjs.extend(utc);
dayjs.extend(relativeTime);
@ -33,7 +33,7 @@ const fetchRecentMovies = async () => {
$("channel")
.children("item")
.slice(0, 6)
.slice(0, 5)
.each((_, element) => {
const title = $(element).children("letterboxd\\:filmTitle").text();
@ -65,4 +65,4 @@ const fetchRecentMovies = async () => {
return recentMovies;
};
module.exports = fetchRecentMovies;
export default fetchRecentMovies;

View file

@ -1,4 +1,4 @@
module.exports = {
export default {
url: process.env.URL || "http://localhost:8080",
siteName: "wonderfulfrog",
siteDescription:
@ -7,9 +7,4 @@ module.exports = {
lang: "en",
author: "Devin Haska",
repoUrl: "https://github.com/wonderfulfrog/wonderfulfrog.com",
social: {
github: "https://github.com/wonderfulfrog",
mastodon: "https://mastodon.social/@wonderfulfrog",
lastfm: "https://www.last.fm/user/wonderfulfrog",
},
};

View file

@ -1,24 +1,41 @@
module.exports = {
export default {
top: [
{
text: "About",
url: "/about",
icon: "circle-info",
},
{
text: "Posts",
url: "/posts",
icon: "list",
},
{
text: "Catalogue",
url: "/catalogue",
text: "Games",
url: "/games",
icon: "game-controller",
},
{
text: "Watching",
url: "/watching",
icon: "tv-retro",
},
{
text: "Books",
url: "/books",
icon: "book",
},
],
bottom: [
{ text: "Blogroll", url: "/blogroll" },
{
text: "Colophon",
url: "/colophon",
},
{ text: "Blogroll", url: "/blogroll" },
{
text: "Contact",
url: "/contact",
},
{
text: "/uses",
url: "/uses",

View file

@ -1,5 +1,5 @@
const { getFontUrl } = require("../utils/fonts");
const fonts = require("../../config/design-tokens/fonts.json");
import { getFontUrl } from "../utils/fonts.js";
import fonts from "../../config/design-tokens/fonts.js";
const preloads = [
{
@ -19,4 +19,4 @@ const preloads = [
},
];
module.exports = preloads;
export default preloads;

View file

@ -1,6 +1,6 @@
require("dotenv").config();
import "dotenv/config";
const EleventyFetch = require("@11ty/eleventy-fetch");
import EleventyFetch from "@11ty/eleventy-fetch";
const accessToken = process.env.DARK_VISITORS_ACCESS_TOKEN;
@ -101,7 +101,7 @@ const fetchRobotsTxt = async () => {
}
};
module.exports = async function () {
export default async function () {
const robotsTxt = await fetchRobotsTxt();
if (!robotsTxt) {
@ -109,4 +109,4 @@ module.exports = async function () {
}
return robotsTxt;
};
}

View file

@ -22,10 +22,10 @@
src="https://cdn.jsdelivr.net/npm/@justinribeiro/lite-youtube@1.4.0/lite-youtube.min.js"></script>
{% endif %}
</head>
<body class="[ flex-col ]">
<body class="flex-col">
{% noRobots %}
{% include "partials/header.html" %}
<main id="main" class="[ flow flex-1 wrapper ]" tabindex="-1">
<main id="main" class="flow flex-1 wrapper" tabindex="-1">
{{ content | safe }}
</main>
{% include "partials/footer.html" %}

View file

@ -0,0 +1,42 @@
---
layout: "layouts/base"
---
{% from "macros/date.njk" import format %}
{% from "macros/utils.njk" import stars %}
{% from "macros/tags.njk" import tagList %}
<header class="flow flow-space-1">
{{ format(page.date) }}
<h1>{{ title }}</h1>
{% if pullquote %}<p class="text-fadeText flow-space-0.25">{{ pullquote }}</p>{% endif %}
</header>
{% if content %}
<section class="flow">
{{ content | safe }}
</section>
{% endif %}
<hr class="my-2" />
<footer class="media-meta-grid gap-1">
<div class="media-image media-image--tall">
<img src="{{ image }}" alt="" />
</div>
<div class="flow flex-col justify-center">
<h2>{{ title }}</h2>
{% if subtitle %}<p class="text-fadeText flow-space-0.5 line-height-m">{{ subtitle }}</p>{% endif %}
{% if rating %}{{ stars(rating) }}{% endif %}
<ul class="list-none p-0 mb-0 media-meta gap-0.5">
{% if author %}
<li class="flex-col">
<strong>Author</strong><span>{{ author }}</span>
</li>
{% endif %}
{% if year %}
<li class="flex-col">
<strong>Published</strong><span>{{ year }}</span>
</li>
{% endif %}
</ul>
</div>
</footer>
<hr class="my-2">
{{ tagList(tags | filter("book") , "/books") }}

View file

@ -1,44 +0,0 @@
---
layout: "layouts/base"
imageAlt: ""
imageCaption: ""
---
{% set filteredTags = tags | filterCatalogueTags %}
{% from "macros/date.njk" import format %}
{% from "macros/utils.njk" import stars %}
<article class="[ catalogue ] [ flow ]">
<header class="[ catalogue-header ] [ flow flow-space-1 ]">
{{ format(page.date) }}
<h1>{{ title }}</h1>
{% if subtitle %}<h2>{{ subtitle }}</h2>{% endif %}
{% if rating %}<div class="[ text-skew ]">{{ stars(rating) }}</div>{% endif %}
{% if tertiary or year %}
<div class="[ cluster text-skew flow-space-1 ]">
{% if tertiary %}
<div class="[ line-height-m ]">
{{ tertiary | safe }}
</div>
{% endif %}
{% if year %}
<p class="[ font-size-s ]"><span class="[ text-fadeText ]">ca.</span> {{ year }}</p>
{% endif %}
</div>
{% endif %}
{% if filteredTags | length > 0 %}
<ul class="[ categories ] [ cluster p-0 flow-space-2 line-height-m ]" role="list">
{% for tag in filteredTags %}<li class="[ flex gap-0.25 ]">{{ tag }}</li>{% endfor %}
</ul>
{% endif %}
</header>
{% if image %}
{% image image, imageAlt, imageCaption, "[ my-3 ]" %}
{% endif %}
{{ content | safe }}
{% if url %}
<a href="{{ url }}" class="[ flex mt-1 items-center gap-0.5 ]" target="_blank" rel="external noreferrer noopener">
{% include "svgs/link.svg" %}{{ linkTitle }}
</a>
{% endif %}
</article>

View file

@ -0,0 +1,49 @@
---
layout: "layouts/base"
---
{% from "macros/date.njk" import format %}
{% from "macros/utils.njk" import stars %}
{% from "macros/tags.njk" import tagList %}
<header class="flow flow-space-1">
{{ format(page.date) }}
<h1>{{ title }}</h1>
<p class="text-fadeText flow-space-0.25">{{ pullquote }}</p>
</header>
{% if content %}
<section class="flow">
{{ content | safe }}
</section>
{% endif %}
<hr class="my-2" />
<footer class="media-meta-grid gap-1">
<div class="media-image media-image--tall">
<img src="{{ image }}" alt="" />
</div>
<div class="flow flex-col justify-center">
<div class="flex items-center gap-0.5 flex-wrap">
<h2>{{ title }}</h2>
</div>
{% if subtitle %}<h2>{{ subtitle }}</h2>{% endif %}
{% if rating %}{{ stars(rating) }}{% endif %}
<ul class="list-none p-0 mb-0 media-meta gap-0.5">
{% if year %}
<li class="flex-col">
<strong>Released</strong><span>{{ year }}</span>
</li>
{% endif %}
{% if platform %}
<li class="flex-col">
<strong>Platform</strong><span>{{ platform }}</span>
</li>
{% endif %}
{% if playtime %}
<li class="flex-col">
<strong>Playtime</strong><span>{{ playtime }}</span>
</li>
{% endif %}
</ul>
</div>
</footer>
<hr class="my-2">
{{ tagList(tags | filter("game") , "/games") }}

View file

@ -0,0 +1,61 @@
---
layout: "layouts/base"
---
{% from "macros/date.njk" import format %}
{% from "macros/utils.njk" import stars %}
{% from "macros/tags.njk" import tagList %}
<header class="flow flow-space-1">
{{ format(page.date) }}
<h1>{{ title }}</h1>
<p class="text-fadeText flow-space-0.25">{{ pullquote }}</p>
</header>
{% if watchHistory.length > 1 %}
<p>
<span class="text-primary">{% include "svgs/circle-info.svg" %}</span>
I've seen this movie <strong>{{ watchHistory.length }}</strong> {{ "time" | pluralize(watchHistory) }}!
</p>
{% endif %}
{% if favourite %}
<p>
<span class="text-secondary">{% include "svgs/star.svg" %}</span>
This is one of my favourite movies!
</p>
{% endif %}
{% if content %}
<section class="flow">
{{ content | safe }}
</section>
{% endif %}
<hr class="my-2" />
<footer class="media-meta-grid gap-1">
<div class="media-image media-image--tall">
<img src="{{ image }}" alt="" />
</div>
<div class="flow flex-col justify-center">
<div class="flex items-center gap-0.5 flex-wrap">
<h2>{{ title }}</h2>
</div>
{% if subtitle %}<h2>{{ subtitle }}</h2>{% endif %}
{% if rating %}{{ stars(rating) }}{% endif %}
<ul class="list-none p-0 mb-0 media-meta gap-0.5">
{% if year %}
<li class="flex-col">
<strong>Released</strong><span>{{ year }}</span>
</li>
{% endif %}
{% if director %}
<li class="flex-col">
<strong>Director</strong>{{ director }}
</li>
{% endif %}
{% if runtime %}
<li class="flex-col">
<strong>Runtime</strong><span>{{ runtime }} mins</span>
</li>
{% endif %}
</ul>
</div>
</footer>
<hr class="my-2">
{{ tagList(tags | filter("movie") , "/watching/movies") }}

View file

@ -3,15 +3,17 @@ layout: "layouts/base"
---
{% from "macros/date.njk" import format %}
<article class="[ flow ]">
<header class="[ flow flow-space-1 mb-2 ]">
<article class="flow">
<header class="flow flow-space-1 mb-2">
{{ format(page.date) }}
<h1>{{ title }}</h1>
<p class="[ text-skew text-fadeText flow-space-0.25 ]">{{ excerpt }}</p>
<ul class="[ categories ] [ cluster list-none p-0 flow-space-2 line-height-m ]">
<p class="text-fadeText flow-space-0.25">{{ excerpt }}</p>
<ul class="categories cluster list-none p-0 flow-space-2 line-height-m">
{% for tag in tags | filter(["post"]) %}
<li class="[ flex gap-0.25 ]">
<a href="/tags/{{ tag | slugify }}">{{ tag }}</a>
<li>
<a class="button" href="/tags/{{ tag | slugify }}">
{% include "svgs/frame.svg" %}
{{ tag }}</a>
</li>
{% endfor %}
</ul>

View file

@ -0,0 +1,56 @@
---
layout: "layouts/base"
---
{% from "macros/date.njk" import format %}
{% from "macros/utils.njk" import stars %}
{% from "macros/tags.njk" import tagList %}
<header class="flow flow-space-1">
{{ format(page.date) }}
<h1>{{ title }}</h1>
<p class="text-fadeText flow-space-0.25">{{ pullquote }}</p>
</header>
{% if content %}
<section class="flow">
{{ content | safe }}
</section>
{% endif %}
{% if favourite or isFavourite %}
<p>
<span class="text-secondary">{% include "svgs/star.svg" %}</span>
This is one of my favourite shows!
</p>
{% endif %}
<hr class="my-2" />
<footer class="media-meta-grid gap-1">
<div class="media-image media-image--tall">
<img src="{{ image }}" alt="" />
</div>
<div class="flow flex-col justify-center">
<div class="flex items-center gap-0.5 flex-wrap">
<h2>{{ title }}</h2>
</div>
{% if subtitle %}<h2>{{ subtitle }}</h2>{% endif %}
{% if director %}
<p class="flex gap-0.5">
<span class="text-fadeText">Directed by</span>{{ director }}
</p>
{% endif %}
{% if watchHistory %}<p class="flow-space-0.25">{{ format(watchHistory | last) }}</p>{% endif %}
{% if rating %}{{ stars(rating) }}{% endif %}
<ul class="list-none p-0 mb-0 media-meta gap-0.5">
{% if year %}
<li class="flex-col">
<strong>Released</strong><span>{{ year }}</span>
</li>
{% endif %}
{% if runtime %}
<li class="flex-col">
<strong>Runtime</strong><span>{{ runtime }} mins</span>
</li>
{% endif %}
</ul>
</div>
</footer>
<hr class="my-2">
{{ tagList(tags | filter("tv") , "/watching/shows") }}

View file

@ -1,36 +0,0 @@
{% from "macros/utils.njk" import stars %}
{% macro one(post, format, showType = true) %}
<article class="[ posts-list-item column-gap-0.5 justify-between line-height-m ]">
<div class="[ cluster gap-0.5 ]"><a href="{{ post.url }}" class="[ line-height-m ]">{{ post.data.title }}</a>
{% if showType %}<p class="[ font-size-s text-fadeText ]">{{ post.data.tags[1] }}</p>{% endif %}</div>
<div class="[ cluster ]">
{% if post.data.rating %}
{{ stars(post.data.rating) }}
{% endif %}
</div>
</article>
{% endmacro %}
{% macro list(posts, showType = true) %}
<ol class="[ flow p-0 ]" role="list">
{% for post in posts %}
<li>
{{ one(post, format, showType) }}
</li>
{% endfor %}
</ol>
{% endmacro %}
{% macro yearList(posts, year, format = "MM/DD", showType = true) %}
<section class="[ flow ]">
<header class="[ cluster gap-0.5 ]">
<h2>{{ year }}</h2>
<p class="[ pill ]" data-state="extrasmall">
{{ posts | length }}
</p>
</header>
{{ list(posts, format) }}
</section>
{% endmacro %}

View file

@ -1,5 +1,6 @@
{% macro format(dateString) %}
<time class="[ date ] [ flex items-center gap-0.5 text-fadeText ]" datetime="{{ date }}">
<span class="[ text-primary line-height-s ]">{% include "svgs/calendar.svg" %}</span>{{ dateString | formatDate("MMMM Do YYYY") }}
<time class="date flex items-center gap-0.5 text-fadeText"
datetime="{{ dateString }}">
<span class="text-primary line-height-s">{% include "svgs/calendar.svg" %}</span>{{ dateString | formatDate("MMMM Do YYYY") }}
</time>
{% endmacro %}

View file

@ -0,0 +1,14 @@
{% macro grid(data, shape = "vertical") %}
<ul class="media-grid {{ shape }} list-none p-0">
{% for item in data %}
<li class="radius-0.5">
<a href="{{ item.url }}">
{% if item.data.image %}<img src="{{ item.data.image }}" alt="" />{% endif %}
<div class="meta font-size-s line-height-s flex items-end px-0.5 pb-0.5">
<span class="meta-text">{{ item.data.title }}</span>
</div>
</a>
</li>
{% endfor %}
</ul>
{% endmacro %}

View file

@ -1,29 +1,21 @@
{% macro one(post, format = "MM/DD") %}
<article class="[ posts-list-item column-gap-0.5 justify-between line-height-l ]">
<a href="{{ post.url }}">{{ post.data.title }}</a>
<time datetime="{{ post.date }}" class="[ text-fadeText ]">{{ post.date | formatDate(format) }}</time>
<p class="[ text-fadeText font-size-s line-height-m ]">{{ post.data.excerpt }}</p>
</article>
{% macro one(post, fmt = "MM/DD") %}
<article class="posts-list-item column-gap-0.5 justify-between line-height-l">
<a href="{{ post.url }}">{{ post.data.title }}</a>
<time datetime="{{ post.date }}" class="text-fadeText">{{ post.date | formatDate(fmt) }}</time>
<p class="text-fadeText font-size-s line-height-m">{{ post.data.excerpt }}</p>
</article>
{% endmacro %}
{% macro list(posts, format = "MM/DD") %}
<ol class="[ flow p-0 ]" role="list">
{% for post in posts %}
<li class="[ flow-space-0.5 ]">
{{ one(post, format) }}
</li>
{% endfor %}
</ol>
<ol class="flow p-0" role="list">
{% for post in posts %}<li class="flow-space-0.5">{{ one(post, format) }}</li>{% endfor %}
</ol>
{% endmacro %}
{% macro yearList(posts, year, format = "MM/DD") %}
<section class="[ flow ]">
<header class="[ cluster gap-0.5 ]">
<h2>{{ year }}</h2>
<p class="[ pill ]" data-state="extrasmall">
{{ posts | length }}
</p>
</header>
{{ list(posts, format) }}
</section>
<section class="flow">
<header class="cluster gap-0.5">
<h2>{{ year }}</h2>
<p class="pill">{{ posts | length }}</p>
</header>
{{ list(posts, format) }}
</section>
{% endmacro %}

View file

@ -0,0 +1,11 @@
{% macro tagList(tags, urlPrefix) %}
<ul class="categories cluster list-none p-0 line-height-m">
{% for tag in tags %}
<li>
<a class="button" href="{{ urlPrefix }}/tag/{{ tag | slugify }}">
{% include "svgs/frame.svg" %}
{{ tag }}</a>
</li>
{% endfor %}
</ul>
{% endmacro %}

View file

@ -1,12 +1,13 @@
{% macro stars(number) %}
{% set filledStars = number %}
{% set emptyStars = 5 - number %}
<ul class="[ stars ] [ flex list-none p-0 m-0 text-primary ]" aria-description="{{ number }} out of 5 stars">
{% for i in range(0, filledStars) %}
<li class="[ star-filled ]">{% include "svgs/star.svg" %}</li>
{% endfor %}
{% for i in range(0, emptyStars) %}
<li class="[ star-empty ]">{% include "svgs/star-empty.svg" %}</li>
{% endfor %}
<ul class="stars flex list-none p-0 mb-0 text-primary"
aria-description="{{ number }} out of 5 stars">
{% for i in range(0, filledStars) %}
<li class="star-filled">{% include "svgs/star.svg" %}</li>
{% endfor %}
{% for i in range(0, emptyStars) %}
<li class="star-empty">{% include "svgs/star-empty.svg" %}</li>
{% endfor %}
</ul>
{% endmacro %}

View file

@ -1,11 +0,0 @@
{% set catalogueTypes = collections.catalogueByType | keys %}
<ul class="[ cluster p-0 gap-0.5 ]" role="list">
<li>
<a class="[ pill ]" href="/catalogue">all<span class="[ pill-count ]">{{ collections.catalogue | length }}</span></a>
</li>
{% for type in catalogueTypes %}
<li>
<a class="[ pill ]" href="/catalogue/{{ type | pluralize }}">{{ type | pluralize }}<span class="[ pill-count ]">{{ collections.catalogueByType[ type ] | length }}</span></a>
</li>
{% endfor %}
</ul>

View file

@ -1,9 +1,9 @@
<footer class="[ bg-surface mt-1.5 py-1.5 ]">
<div class="[ wrapper flow ]">
<div class="[ repel ]">
<footer class="bg-surface mt-1.5 py-1.5">
<div class="wrapper flow">
<div class="repel">
<p>&copy; {{ meta.author }} 2018 - 2025.</p>
<nav>
<ul class="[ cluster p-0 m-0 line-height-m ]" role="list">
<ul class="cluster p-0 m-0 line-height-m" role="list">
{% for link in navigation.bottom %}
<li>
<a href="{{ link.url }}">{{ link.text }}</a>
@ -12,33 +12,40 @@
</ul>
</nav>
</div>
<div class="[ repel ]">
<nav class="[ flow-space-0.5 ]">
<ul class="[ menu ] [ cluster p-0 m-0 gap-0.5 ]" role="list">
<div class="repel">
<nav class="flow-space-0.5">
<ul class="cluster p-0 m-0 gap-0.5" role="list">
<li>
<span class="[ visually-hidden ]">Feeds</span>
<a href="/feeds" class="[ button ]" aria-label="View RSS feed">{% include "svgs/rss.svg" %}</a>
<a href="/feeds"
class="button button--alt"
aria-label="Feeds"
title="Feeds">{% include "svgs/rss.svg" %}</a>
</li>
<li>
<a href="https://github.com/wonderfulfrog"
class="button button--alt"
target="_blank"
rel="me external noreferrer noopener"
title="GitHub"
aria-label="GitHub">{% include "svgs/social-github.svg" %}</a>
</li>
<li>
<a href="https://mastodon.social/@wonderfulfrog"
class="button button--alt"
target="_blank"
rel="me external noreferrer noopener"
title="Mastodon"
aria-label="Mastodon">{% include "svgs/social-mastodon.svg" %}</a>
</li>
{% for key, url in meta.social %}
{% set alt = key | capitalize %}
<li>
<span class="[ visually-hidden ]">{{ alt }}</span>
<a href="{{ url }}"
class="[ button ]"
target="_blank"
rel="me external noreferrer noopener"
aria-label="View {{ alt }} page">{% include "svgs/social-" + key + ".svg" %}</a>
</li>
{% endfor %}
</ul>
</nav>
<ul class="[ cluster p-0 m-0 ]" role="list">
<li>
<a href="/webrings">Webrings</a>
</li>
<ul class="cluster p-0 m-0" role="list">
<li>
<a href="/changelog">Changelog</a>
</li>
<li>
<a href="/webrings">Webrings</a>
</li>
<li>
<a href="{{ meta.repoUrl }}"
target="_blank"

View file

@ -1,16 +1,24 @@
<header class="[ navbar ][ py-1 mb-2 ]">
<header class="navbar py-1 mb-2">
<a href="#main">Skip to content</a>
<div class="[ wrapper ]">
<nav class="[ repel ]">
<div class="[ site-logo ] [ flex items-center gap-0.5 ]">
<a class="[ logo ] [ flex items-center justify-center ]"
<div class="wrapper">
<nav class="repel">
<div class="site-logo flex items-center gap-0.5">
<a class="logo flex items-center justify-center"
aria-label="Go to index"
href="{{ meta.url }}">
<img src="/assets/images/logo.svg" width="32" height="32" alt="" />
<img eleventy:ignore
src="/assets/images/logo.svg"
width="32"
height="32"
alt="" />
</a>
<a class="[ wordmark ] [ flex ]"
href="{{ meta.url }}"
aria-label="Go to index">
<a class="wordmark flex" href="{{ meta.url }}" aria-label="Go to index">
<svg height="0" width="0">
<filter id="3d_text" color-interpolation-filters="sRGB">
<feDropShadow dx="0" dy="1" stdDeviation="0" flood-opacity="1" flood-color="var(--color-shadow)" />
<feDropShadow dx="0" dy="2" stdDeviation="0" flood-opacity="1" flood-color="var(--color-shadow)" />
</filter>
</svg>
<span>wonderful</span>
<span>frog</span>
</a>

View file

@ -1,7 +1,10 @@
<ul class="[ menu ] [ list-none cluster p-0 m-0 ]">
<ul class="menu list-none cluster p-0 m-0">
{% for item in navigation.top %}
<li>
<a class="[ button ]" href="{{ item.url }}">{{ item.text }}</a>
<a class="button button--alt font-size-s line-height-m"
href="{{ item.url }}"
title="{{ item.text }}"
aria-label="{{ item.text }}">{% include "svgs/" + item.icon + ".svg" %}</a>
</li>
{% endfor %}
</ul>

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 0C3.34315 0 2 1.34315 2 3V13C2 14.6569 3.34315 16 5 16H14V14H4V12H14V0H5Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 212 B

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8ZM7 8H6V6H9V11H10V13H7V8ZM9 5V3H7V5H9Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 321 B

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 15V12H10V15H12V12H15V10H12V6H15V4H12V1H10V4H6V1H4V4H1V6H4V10H1V12H4V15H6ZM10 10H6V6H10V10Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 269 B

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 13L3 14L6 11H10L13 14L16 13L15.248 4.7284C15.1076 3.18316 13.812 2 12.2604 2H3.73964C2.18803 2 0.89244 3.18316 0.751964 4.72839L0 13ZM12 6C12.5523 6 13 5.55228 13 5C13 4.44772 12.5523 4 12 4C11.4477 4 11 4.44772 11 5C11 5.55228 11.4477 6 12 6ZM12 8C12 8.55228 11.5523 9 11 9C10.4477 9 10 8.55228 10 8C10 7.44772 10.4477 7 11 7C11.5523 7 12 7.44772 12 8ZM5 8C6.10457 8 7 7.10457 7 6C7 4.89543 6.10457 4 5 4C3.89543 4 3 4.89543 3 6C3 7.10457 3.89543 8 5 8Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 633 B

View file

@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 1H1V3H3V1Z" fill="currentColor"/>
<path d="M3 5H1V7H3V5Z" fill="currentColor"/>
<path d="M1 9H3V11H1V9Z" fill="currentColor"/>
<path d="M3 13H1V15H3V13Z" fill="currentColor"/>
<path d="M15 1H5V3H15V1Z" fill="currentColor"/>
<path d="M15 5H5V7H15V5Z" fill="currentColor"/>
<path d="M5 9H15V11H5V9Z" fill="currentColor"/>
<path d="M15 13H5V15H15V13Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 486 B

View file

@ -1,3 +0,0 @@
<svg fill="currentColor" width="16" height="16" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path d="M14.131 22.948l-1.172-3.193c0 0-1.912 2.131-4.771 2.131-2.537 0-4.333-2.203-4.333-5.729 0-4.511 2.276-6.125 4.515-6.125 3.224 0 4.245 2.089 5.125 4.772l1.161 3.667c1.161 3.561 3.365 6.421 9.713 6.421 4.548 0 7.631-1.391 7.631-5.068 0-2.968-1.697-4.511-4.844-5.244l-2.344-0.511c-1.624-0.371-2.104-1.032-2.104-2.131 0-1.249 0.985-1.984 2.604-1.984 1.767 0 2.704 0.661 2.865 2.24l3.661-0.444c-0.297-3.301-2.584-4.656-6.323-4.656-3.308 0-6.532 1.251-6.532 5.245 0 2.5 1.204 4.077 4.245 4.807l2.484 0.589c1.865 0.443 2.484 1.224 2.484 2.287 0 1.359-1.323 1.921-3.828 1.921-3.703 0-5.244-1.943-6.124-4.625l-1.204-3.667c-1.541-4.765-4.005-6.531-8.891-6.531-5.287-0.016-8.151 3.385-8.151 9.192 0 5.573 2.864 8.595 8.005 8.595 4.14 0 6.125-1.943 6.125-1.943z"/>
</svg>

Before

Width:  |  Height:  |  Size: 875 B

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 2.96338L5.75926 0.349182L4.24074 1.65076L6.25437 4H1V15H15V4H9.74563L11.7593 1.65076L10.2407 0.349182L8 2.96338ZM11 6H3V13H11V6Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 307 B

View file

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 7C9.65685 7 11 5.65685 11 4C11 2.34315 9.65685 1 8 1C6.34315 1 5 2.34315 5 4C5 5.65685 6.34315 7 8 7Z" fill="currentColor"/>
<path d="M14 12C14 10.3431 12.6569 9 11 9H5C3.34315 9 2 10.3431 2 12V15H14V12Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 342 B

View file

@ -1,3 +0,0 @@
{
"excludeFromSitemap": true
}

View file

@ -1,101 +0,0 @@
---
permalink: /admin/config.yml
---
backend:
name: github
repo: wonderfulfrog/wonderfulfrog.com
branch: main
squash_merges: true
commit_messages:
create: {% raw %}"feat: add {{collection}} `{{slug}}`"{% endraw %}
update: {% raw %}"feat: update {{collection}} `{{slug}}`"{% endraw %}
delete: {% raw %}"feat: remove {{collection}} `{{slug}}`"{% endraw %}
media_folder: ""
publish_mode: editorial_workflow
collections:
- name: "post"
label: "Post"
folder: "src/posts"
create: true
slug: "{% raw %}{{year}}-{{month}}-{{day}}-{{slug}}{% endraw %}"
fields:
- { label: "Title", name: "title", widget: "string" }
- { label: "Excerpt", name: "excerpt", widget: "string" }
- { label: "Tags", name: "tags", widget: "list" }
- { label: "Using YouTube", name: "youtube", widget: "boolean", default: false, hint: "If using the YouTube shortcode, enable this option to append the lite-youtube script." }
- { label: "Body", name: "body", widget: "markdown" }
- label: "Page"
name: "page"
create: false
files:
{% for page in collections.page %}
- label: "{{ page.data.title }}"
name: "{{ page.fileSlug }}"
file: "{{ page.inputPath | replace("./", "") }}"
fields:
- { label: "Title", name: "title", widget: "string" }
- { label: "Permalink", name: "permalink", widget: "string" }
- { label: "Layout", name: "layout", widget: "hidden", default: "layouts/base" }
- { label: "Using YouTube", name: "youtube", widget: "boolean", default: false, hint: "If using the YouTube shortcode, enable this option to append the lite-youtube script." }
- { label: "Body", name: "body", widget: "markdown" }
{% endfor %}
- name: "book"
label: "Book"
folder: "src/catalogue/books"
slug: "{% raw %}{{year}}-{{month}}-{{day}}-{{slug}}{% endraw %}"
create: true
fields:
- { label: "Title", name: "title", widget: "string" }
- { label: "Subtitle", name: "subtitle", widget: "string" }
- { label: "Author", name: "author", widget: "string" }
- { label: "Rating", name: "rating", widget: "number", min: 1, max: 5 }
- { label: "Image", name: "image", widget: "string", hint: "A CDN URL."}
- { label: "Tags", name: "tags", widget: "list" }
- { label: "Using YouTube", name: "youtube", widget: "boolean", default: false, hint: "If using the YouTube shortcode, enable this option to append the lite-youtube script." }
- { label: "Body", name: "body", widget: "markdown" }
- name: "game"
label: "Game"
folder: "src/catalogue/games"
slug: "{% raw %}{{year}}-{{month}}-{{day}}-{{slug}}{% endraw %}"
create: true
fields:
- { label: "Title", name: "title", widget: "string" }
- { label: "Subtitle", name: "subtitle", widget: "string" }
- { label: "Year", name: "year", widget: "string" }
- { label: "Rating", name: "rating", widget: "number", min: 1, max: 5 }
- { label: "Image", name: "image", widget: "string", hint: "A CDN URL."}
- { label: "Tags", name: "tags", widget: "list" }
- { label: "Using YouTube", name: "youtube", widget: "boolean", default: false, hint: "If using the YouTube shortcode, enable this option to append the lite-youtube script." }
- { label: "Body", name: "body", widget: "markdown" }
- name: "comic"
label: "Comic"
folder: "src/catalogue/comics"
slug: "{% raw %}{{year}}-{{month}}-{{day}}-{{slug}}{% endraw %}"
create: true
fields:
- { label: "Title", name: "title", widget: "string" }
- { label: "Publisher", name: "publisher", widget: "string" }
- { label: "Author", name: "author", widget: "string" }
- { label: "Year", name: "year", widget: "string" }
- { label: "Rating", name: "rating", widget: "number", min: 1, max: 5 }
- { label: "Image", name: "image", widget: "string", hint: "A CDN URL."}
- { label: "Tags", name: "tags", widget: "list" }
- { label: "Using YouTube", name: "youtube", widget: "boolean", default: false, hint: "If using the YouTube shortcode, enable this option to append the lite-youtube script." }
- { label: "Body", name: "body", widget: "markdown" }
- name: "podcast"
label: "Podcast"
folder: "src/catalogue/podcasts"
slug: "{% raw %}{{year}}-{{month}}-{{day}}-{{slug}}{% endraw %}"
create: true
fields:
- { label: "Title", name: "title", widget: "string" }
- { label: "URL", name: "url", widget: "string", hint: "A URL for someone to listen to the podcast." }
- { label: "Rating", name: "rating", widget: "number", min: 1, max: 5 }
- { label: "Image", name: "image", widget: "string", hint: "A CDN URL."}
- { label: "Tags", name: "tags", widget: "list" }
- { label: "Using YouTube", name: "youtube", widget: "boolean", default: false, hint: "If using the YouTube shortcode, enable this option to append the lite-youtube script." }
- { label: "Body", name: "body", widget: "markdown" }

View file

@ -1,21 +0,0 @@
---
permalink: admin/index.html
---
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="noindex" />
<!-- Making Decap more responsive in mobile -->
<!-- https://github.com/hithismani/responsive-decap -->
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/hithismani/responsive-decap@main/dist/responsive.min.css">
<title>Content Manager</title>
</head>
<body>
<!-- Include the script that builds the page and powers Decap CMS -->
<script src="https://unpkg.com/decap-cms@^3.0.0/dist/decap-cms.js"></script>
</body>
</html>

View file

@ -1,10 +0,0 @@
module.exports = {
layout: "layouts/catalogue-item",
tags: "book",
permalink: "catalogue/books/{{ page.fileSlug }}/index.html",
linkTitle: "View book details",
eleventyComputed: {
tertiary: (data) =>
`<p class="[ flow-space-0.5 ]"><span class="[ text-fadeText ]">by</span> ${data.author}</p>`,
},
};

View file

@ -1,6 +0,0 @@
module.exports = {
tags: "catalogue",
eleventyComputed: {
description: (data) => `My thoughts on ${data.title}.`,
},
};

View file

@ -1,10 +0,0 @@
---
title: "The Monster Sisters"
publisher: "Orca Books"
author: "Gareth Gaudin"
year: 2021
isbn: 9781459822290
rating: 4
image: https://cdn.wonderfulfrog.com/images/the-monster-sisters-vol-2.jpg
tags: ["adventure"]
---

View file

@ -1,16 +0,0 @@
---
title: "Monstress: Awakening"
publisher: Image Comics
author: Majorie Liu, Sana Takeda
volume: 1
year: 2017
image: https://cdn.wonderfulfrog.com/images/monstress-vol-1.jpeg
rating: 3
tags: ["fantasy"]
---
The first 75% of the volume is quite confusing. Its like reading the second or third volume in a series without reading the first. I get starting with a mystery as a hook, but when every second word is something new, I get confused!
Im not convinced that comics are the best form of world building, at least through expository dialogue. When several entire pages that are at least half-filled with text, I think theres a problem.
The artwork is praised often elsewhere and I must agree - every page (even panel) could stand on its own. Absolutely stunning. The world building on the visual level absolutely nails it. If atmosphere is set within the first few pages and Im here for it.

View file

@ -1,17 +0,0 @@
---
title: "Monstress: The Blood"
publisher: Image Comics
author: Majorie Lui, Sana Takeda
volume: 2
year: 2017
image: https://cdn.wonderfulfrog.com/images/monstress-vol-2-tp_77ea1f170a.jpg
isbn: 9781534300415
rating: 3
tags: ["fantasy"]
---
I love the cast of the story. We are introduced to some new characters with a heavy nautical theme. Old Tooth the shark-person looks straight out of the 90s (remember Street Sharks?). The artwork continues to be the main selling point for me.
Where things falter though has to be the writing (again). There are less mysteries to track and more emphasis on character development, but Im finding it so dense that its hard to follow. It practically requires a wiki open while reading. There is so much to pack into a graphic novel with limited pages. I think the series would do better as a novel if Im completely honest.
I cant get into the story, but Sana Takedas art is so phenomenal I want to keep going.

View file

@ -1,9 +0,0 @@
module.exports = {
layout: "layouts/catalogue-item",
tags: "comic",
permalink: "catalogue/comics/{{ page.fileSlug }}/index.html",
eleventyComputed: {
tertiary: (data) =>
`<p class="[ flow-space-0.5 ]"><span class="[ text-fadeText ]">by</span> ${data.author}</p>`,
},
};

View file

@ -1,7 +0,0 @@
---
title: Axiom Verge
platform: Nintendo Switch
image: https://cdn.wonderfulfrog.com/images/Axiom_Verge_Title.png
tags: ["action", "metroidvania", "indie", "single player"]
year: 2015
---

View file

@ -1,8 +0,0 @@
module.exports = {
layout: "layouts/catalogue-item",
tags: "game",
permalink: "catalogue/games/{{ page.fileSlug }}/index.html",
eleventyComputed: {
subtitle: (data) => `${data.platform}`,
},
};

View file

@ -1,10 +0,0 @@
---
title: Hunting Warhead
rating: 5
image: https://cdn.wonderfulfrog.com/images/hunting-warhead.webp
url: https://www.cbc.ca/listen/cbc-podcasts/387-hunting-warhead
---
A chilling and gripping history telling the story of how one of the largest networks of CSAM was taken down by tracking its biggest maintainer.
[Listen to the podcast](https://www.cbc.ca/listen/cbc-podcasts/387-hunting-warhead)

View file

@ -1,15 +0,0 @@
---
title: Maintenance Phase
rating: 5
image: https://cdn.wonderfulfrog.com/images/maintenance-phase.webp
url: https://www.maintenancephase.com/
---
A phenomenal podcast that challenged everything I knew about fatness, dieting, food, and my own relationship with each. I don't say this lightly but it has been a life-changing revelation for me. I devour every new episode with fervor and eagerly consume all their content.
## Favourite episodes
- [Dr. Oz](https://maintenancephase.buzzsprout.com/1411126/7857472-dr-oz)
- [Halo Top Ice Cream](https://maintenancephase.buzzsprout.com/1411126/7127890-halo-top-ice-cream)
- [The Biggest Loser](https://maintenancephase.buzzsprout.com/1411126/7353850-the-biggest-loser)
I used to watch this show religiously with my parents. I'd look to it at times for inspiration. Realizing the literal hell the cast when through now is horrifying.

View file

@ -1,13 +0,0 @@
---
title: Reply All
url: https://gimletmedia.com/shows/reply-all
rating: 3
image: https://cdn.wonderfulfrog.com/images/reply-all.jpeg
---
There are too many episodes to this podcast, but I have been shared a few interesting ones from my partner. I was never a regular listener, and have not listened since it [effectively imploded](https://www.vulture.com/article/gimlet-reply-all-controversy-spotify-test-kitchen.html)
## Favourite episodes
- [The Case of the Missing Hit](https://pca.st/episode/cc572c51-e2bd-41fe-a138-d4f8ecba3549)
This is a real journey. There are so many fun twists and turns.

View file

@ -1,6 +0,0 @@
module.exports = {
layout: "layouts/catalogue-item",
tags: "podcast",
permalink: "catalogue/podcasts/{{ page.fileSlug }}/index.html",
linkTitle: "Listen to the podcast",
};

View file

@ -4,7 +4,10 @@ author: Brandon Sanderson
isbn: 9780765326355
rating: 5
image: https://cdn.wonderfulfrog.com/images/the-way-of-kings.jpeg
tags: ["fantasy"]
isFavourite: true
tags:
- fantasy
- fiction
year: 2010
---

View file

@ -5,7 +5,9 @@ author: Seth Godin
isbn: 9781591841661
rating: 4
image: https://cdn.wonderfulfrog.com/images/the-dip.jpeg
tags: ["self help"]
tags:
- self-help
- non-fiction
year: 2007
---

View file

@ -5,7 +5,9 @@ author: Mark Manson
isbn: 9780062457714
rating: 5
image: https://cdn.wonderfulfrog.com/images/subtle-art.jpeg
tags: ["self help"]
tags:
- self-help
- non-fiction
year: 2016
---

View file

@ -3,9 +3,11 @@ title: The Total Money Makeover
subtitle: A Proven Plan for Financial Fitness
author: Dave Ramsey
isbn: 9780785289081
rating: 3
rating: 2
image: https://cdn.wonderfulfrog.com/images/total-money-makeover.webp
tags: ["finance"]
tags:
- non-fiction
- finance
year: 2003
---

View file

@ -5,7 +5,10 @@ author: Gary Taubes
isbn: 9780307272706
rating: 3
image: https://cdn.wonderfulfrog.com/images/why-we-get-fat.jpeg
tags: ["self help", "diet"]
tags:
- non-fiction
- self-help
- diet
year: 2010
---

View file

@ -3,9 +3,13 @@ title: Digital Minimalism
subtitle: Choosing a Focused Life in a Noisy World
author: Cal Newport
isbn: 9780525536512
rating: 4
rating: 2
image: https://cdn.wonderfulfrog.com/images/digital-minimalism.jpg
tags: ["white guy productivity", "technology"]
tags:
- non-fiction
- self-help
- technology
- white-guy-productivity
year: 2019
---

View file

@ -4,7 +4,11 @@ author: Brené Brown
isbn: 9781473562523
rating: 5
image: https://cdn.wonderfulfrog.com/images/dare-to-lead.jpeg
tags: ["leadership", "self help", "business"]
tags:
- non-fiction
- self-help
- leadership
- business
year: 2018
---

View file

@ -5,7 +5,12 @@ author: Edmond Lau
isbn: 9780996128100
rating: 4
image: https://cdn.wonderfulfrog.com/images/the-effective-engineer.jpeg
tags: ["leadership", "software development", "technology"]
tags:
- non-fiction
- self-help
- leadership
- technology
- software-development
year: 2015
---

View file

@ -3,9 +3,12 @@ title: Atomic Habits
subtitle: An Easy & Proven Way to Build Good Habits & Break Bad Ones
author: James Clear
isbn: 9780735211292
rating: 5
rating: 3
image: https://cdn.wonderfulfrog.com/images/atomic-habits.jpeg
tags: ["psychology", "self help"]
tags:
- non-fiction
- self-help
- psychology
year: 2018
---

View file

@ -4,7 +4,10 @@ author: Ryan Holiday
isbn: 9781591847816
rating: 3
image: https://cdn.wonderfulfrog.com/images/ego-is-the-enemy.jpeg
tags: ["stoicism", "self help"]
tags:
- non-fiction
- self-help
- stoicism
year: 2016
---

View file

@ -4,7 +4,9 @@ author: Marcus Aurelius
isbn: 9780140449334
rating: 2
image: https://cdn.wonderfulfrog.com/images/meditations.jpeg
tags: ["stoicism"]
tags:
- non-fiction
- stoicism
year: 180
---

View file

@ -5,7 +5,10 @@ author: Cal Newport
isbn: 9781455586691
rating: 2
image: https://cdn.wonderfulfrog.com/images/deep-work.jpeg
tags: ["white guy productivity"]
tags:
- non-fiction
- self-help
- white-guy-productivity
year: 2016
---

View file

@ -5,7 +5,10 @@ author: Matthew Walker
isbn: 9781501144318
rating: 5
image: https://cdn.wonderfulfrog.com/images/why-we-sleep.jpeg
tags: ["sleep"]
tags:
- non-fiction
- sleep
- science
year: 2017
---

View file

@ -4,7 +4,9 @@ author: Neil deGrasse Tyson
isbn: 9780393609394
rating: 2
image: https://cdn.wonderfulfrog.com/images/astro-in-hurry.jpeg
tags: ["science"]
tags:
- non-fiction
- science
year: 2017
---

View file

@ -4,7 +4,10 @@ author: Angie Thomas
isbn: 9780062498533
rating: 5
image: https://cdn.wonderfulfrog.com/images/the-hate-u-give.jpg
tags: ["fiction", "racism"]
isFavourite: true
tags:
- fiction
- racism
year: 2017
---

View file

@ -5,7 +5,11 @@ author: Desmond Cole
isbn: 9780385686341
rating: 5
image: https://cdn.wonderfulfrog.com/images/the-skin-were-in.jpeg
tags: ["politics", "canada", "racism"]
tags:
- non-fiction
- racism
- canada
- politics
year: 2020
---

View file

@ -5,7 +5,9 @@ author: Gay Hendricks
isbn: 9780061735363
rating: 3
image: https://cdn.wonderfulfrog.com/images/the-big-leap.jpg
tags: ["self help"]
tags:
- non-fiction
- self-help
year: 2009
---

View file

@ -4,7 +4,10 @@ author: Joshua Whitehead
isbn: 9781551527253
rating: 4
image: https://cdn.wonderfulfrog.com/images/jonny-appleseed.jpeg
tags: ["fictional", "emotional"]
tags:
- fiction
- emotional
- lgbtq
year: 2018
---

View file

@ -1,16 +1,19 @@
---
title: Understanding Comics
subtitle: The Invisible Art
date: 2021-09-09T10:00:00-07:00
author: Scott McCloud
isbn: 9780060976255
rating: 5
image: https://cdn.wonderfulfrog.com/images/understanding-comics.jpg
tags: ["comics", "art", "history"]
tags:
- comics
- art
- history
isFavourite: true
year: 1993
---
I debated if this should end up in a different catalogue, but it deserves to be in both, if anything.
If you wondered just how deep the rabbit hole goes when it comes to all things comics, Scott McCloud is here to tell you it goes _deep_. Anyone who may doubt the medium of the comic -- I challenge you to come out without a new perspective reading this. I know I did. I appreciate them so much more now.
To call it "a book about comics" feels like a disservice. There is so much more going on here about history and the human mind.

View file

@ -5,7 +5,9 @@ author: Victoria Ortiz
isbn: 978054497364
rating: 4
image: https://cdn.wonderfulfrog.com/images/dissenter-on-the-bench.jpeg
tags: ["biography"]
tags:
- non-fiction
- biography
year: 2019
---

View file

@ -4,7 +4,9 @@ author: Jim Butcher
image: https://cdn.wonderfulfrog.com/images/jim-butcher-peace-talks.jpeg
isbn: 9780393609394
rating: 2
tags: ["fiction", "fantasy"]
tags:
- fiction
- fantasy
year: 2020
---

View file

@ -5,7 +5,10 @@ image: https://cdn.wonderfulfrog.com/images/9780807041307_p0_v1_s1200x630.jpg
url: https://www.yourfatfriend.com/book
isbn: 9780807041307
rating: 5
tags: ["anti-fat"]
tags:
- non-fiction
- diet
- anti-fat
year: 2020
---

View file

@ -5,7 +5,11 @@ author: Johann Hari
isbn: 9781526620224
rating: 4
image: https://cdn.wonderfulfrog.com/images/stolen-focus.jpg
tags: ["focus", "adhd", "psychology"]
tags:
- non-fiction
- adhd
- focus
- psychology
year: 2022
---

View file

@ -0,0 +1,5 @@
export default {
layout: "layouts/book",
permalink: "books/{{ page.fileSlug }}/index.html",
tags: "book",
};

View file

@ -1,8 +1,11 @@
---
title: "Super Mario World 2: Yoshi's Island"
platform: Super Nintendo
image: https://cdn.wonderfulfrog.com/images/Yoshis_Island_box_art.jpg
tags: ["platformer"]
image: https://cdn.wonderfulfrog.com/images/games/yoshis-island-cover.jpg
tags:
- single-player
- platformer
isFavourite: true
year: 1995
---

View file

@ -2,7 +2,9 @@
title: "Majora's Mask 3D"
platform: Nintendo 3DS
image: https://cdn.wonderfulfrog.com/images/Majoras_Mask_3D_cover.png
tags: ["adventure", "single player"]
tags:
- single-player
- adventure
year: 2015
---

View file

@ -2,7 +2,10 @@
title: "Mario & Luigi: Dream Team"
platform: Nintendo 3DS
image: https://cdn.wonderfulfrog.com/images/MarioLuigiDreamTeam.jpg
tags: ["rpg", "turn based", "single player"]
tags:
- single-player
- rpg
- turn-based
year: 2013
---

View file

@ -2,13 +2,17 @@
title: "Donkey Kong Country: Tropical Freeze"
platform: Nintendo Wii U
image: https://cdn.wonderfulfrog.com/images/DKC5_box_art.jpg
tags: ["platformer", "challenging", "single player", "multiplayer"]
tags:
- single-player
- co-op
- platformer
- challenging
year: 2014
---
Another challenging game. DKCTF nails the difficulty perfectly. As mentioned before, every failure feels like it's 100% the fault of the player. The controls are spot on and frankly should be the model for any platformer going forward. There are various control schemes to suit practically any player. My only minor gripe was that the buttons for each scheme were not customizable -- but the default options are fine. Going from the first level to the last is fairly short experience, but in typical DKC fashion the meat of the game comes from replaying levels and grabbing all the collectables. If that's not really your thing then you've still got a solid platformer that should scratch the itch. Visually the game is amazing to look at... I mean just look at this:
{% image "https://cdn.wonderfulfrog.com/images/dkctf.jpg", "", "" %}
![Screenshot of Donkey Kong Country: Tropical Freeze. DK and Diddy are swimming underwater.](https://cdn.wonderfulfrog.com/images/dkctf.jpg)
The soundtrack is all aces as well -- composed by the same guy behind the first games - David Wise. It provides a fitting background - intense enough that it energizes you, but not overbearing that it comes the star of the show.

View file

@ -2,7 +2,11 @@
title: "Professor Layton vs. Phoenix Wright: Ace Attorney"
platform: Nintendo 3DS
image: https://cdn.wonderfulfrog.com/images/Laytonvsaceattorneycover.jpg
tags: ["puzzle", "narrative", "visual novel", "single player"]
tags:
- single-player
- narrative
- puzzle
- visual-novel
year: 2014
---

View file

@ -2,7 +2,11 @@
title: "The Wonderful 101"
platform: Nintendo Wii U
image: https://cdn.wonderfulfrog.com/images/Wonderful_101_box_artwork.jpg
tags: ["action", "beat em up", "single player"]
tags:
- single-player
- action
- beat-em-up
- platinum
year: 2013
---

View file

@ -2,7 +2,10 @@
title: Fallout 4
platform: PC
image: https://cdn.wonderfulfrog.com/images/Fallout_4_cover_art.jpg
tags: ["rpg", "action", "single player"]
tags:
- single-player
- action
- rpg
year: 2015
---

View file

@ -2,7 +2,11 @@
title: "Persona 3 Portable"
platform: PlayStation Portable
image: https://cdn.wonderfulfrog.com/images/p3p.jpg
tags: ["rpg", "turn based", "persona", "single player"]
tags:
- single-player
- rpg
- turn-based
- persona
youtube: true
year: 2009
---

View file

@ -2,7 +2,10 @@
title: Shovel Knight
platform: Nintendo Wii U
image: https://cdn.wonderfulfrog.com/images/Shovel_knight_cover.jpg
tags: ["platformer", "retro", "single player"]
tags:
- single-player
- platformer
- retro
year: 2014
---

View file

@ -2,7 +2,12 @@
title: Tomb Raider
platform: PC
image: https://cdn.wonderfulfrog.com/images/TombRaider2013.jpg
tags: ["action", "adventure", "puzzle", "reboot", "single player"]
tags:
- single-player
- action
- adventure
- puzzle
- reboot
year: 2013
---

Some files were not shown because too many files have changed in this diff Show more