feat: added a tag to allow for featured notes
This commit is contained in:
+3
-2
@@ -2,6 +2,7 @@ import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight";
|
|||||||
import markdownIt from "markdown-it";
|
import markdownIt from "markdown-it";
|
||||||
import markdownItAnchor from "markdown-it-anchor";
|
import markdownItAnchor from "markdown-it-anchor";
|
||||||
import { filters } from "./src/_filters/index.js";
|
import { filters } from "./src/_filters/index.js";
|
||||||
|
import { postComparator } from "./src/_utils/sort-posts.js";
|
||||||
|
|
||||||
export default function (eleventyConfig) {
|
export default function (eleventyConfig) {
|
||||||
// 1. Static Asset Passthrough Copy
|
// 1. Static Asset Passthrough Copy
|
||||||
@@ -38,12 +39,12 @@ export default function (eleventyConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 5. Custom Collections
|
// 5. Custom Collections
|
||||||
// Chronological blog posts collection
|
// Pinned / featured prioritized, chronological blog posts collection
|
||||||
eleventyConfig.addCollection("posts", (collectionApi) => {
|
eleventyConfig.addCollection("posts", (collectionApi) => {
|
||||||
return collectionApi
|
return collectionApi
|
||||||
.getFilteredByGlob("src/posts/*.md")
|
.getFilteredByGlob("src/posts/*.md")
|
||||||
.filter((post) => !post.data.draft)
|
.filter((post) => !post.data.draft)
|
||||||
.sort((a, b) => b.date - a.date);
|
.sort(postComparator);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dynamic Tag List Collection across all posts
|
// Dynamic Tag List Collection across all posts
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* Safely extracts a numeric timestamp from an Eleventy item or its frontmatter data.
|
||||||
|
* @param {object} item Eleventy page / post object
|
||||||
|
* @returns {number} Unix timestamp in ms
|
||||||
|
*/
|
||||||
|
export function getPostTimestamp(item) {
|
||||||
|
if (!item || typeof item !== "object") return 0;
|
||||||
|
const rawDate = item.date ?? item.data?.date ?? item.data?.created;
|
||||||
|
if (rawDate instanceof Date) {
|
||||||
|
const t = rawDate.getTime();
|
||||||
|
return Number.isNaN(t) ? 0 : t;
|
||||||
|
}
|
||||||
|
if (typeof rawDate === "string" || typeof rawDate === "number") {
|
||||||
|
const t = new Date(rawDate).getTime();
|
||||||
|
return Number.isNaN(t) ? 0 : t;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether a post is flagged as pinned or featured.
|
||||||
|
* Explicit boolean true or truthy values are supported;
|
||||||
|
* missing, null, or false values return false.
|
||||||
|
*
|
||||||
|
* @param {object} item Eleventy page / post object
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isPostPinned(item) {
|
||||||
|
if (!item || typeof item !== "object") return false;
|
||||||
|
const data = item.data;
|
||||||
|
if (!data || typeof data !== "object") return false;
|
||||||
|
return Boolean(data.pinned || data.featured);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comparator for sorting blog posts:
|
||||||
|
* 1. Pinned or featured posts are prioritized at the top of the collection.
|
||||||
|
* 2. Within the same priority group (both pinned or both unpinned), posts
|
||||||
|
* are sorted chronologically in descending order (newest first).
|
||||||
|
*
|
||||||
|
* @param {object} a First post
|
||||||
|
* @param {object} b Second post
|
||||||
|
* @returns {number} Negative if a comes before b, positive if b comes before a, 0 if equal
|
||||||
|
*/
|
||||||
|
export function postComparator(a, b) {
|
||||||
|
const aPinned = isPostPinned(a);
|
||||||
|
const bPinned = isPostPinned(b);
|
||||||
|
|
||||||
|
if (aPinned !== bPinned) {
|
||||||
|
return bPinned ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeA = getPostTimestamp(a);
|
||||||
|
const timeB = getPostTimestamp(b);
|
||||||
|
|
||||||
|
return timeB - timeA;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default postComparator;
|
||||||
@@ -45,7 +45,7 @@ test("Build & Link Integrity: Output inspection and link verification", () => {
|
|||||||
// 2. HTML Boilerplate Invariants
|
// 2. HTML Boilerplate Invariants
|
||||||
assert.match(content, /<!DOCTYPE html>/i, `${relHtml} must contain <!DOCTYPE html>`);
|
assert.match(content, /<!DOCTYPE html>/i, `${relHtml} must contain <!DOCTYPE html>`);
|
||||||
assert.match(content, /<meta\s+name="viewport"/i, `${relHtml} must have viewport meta tag`);
|
assert.match(content, /<meta\s+name="viewport"/i, `${relHtml} must have viewport meta tag`);
|
||||||
assert.match(content, /<link\s+rel="stylesheet"\s+href="\/assets\/css\/main\.css">/i, `${relHtml} must link to main.css`);
|
assert.match(content, /<link\s+rel="stylesheet"\s+href="\/assets\/css\/main\.css(?:\?[^"]*)?">/i, `${relHtml} must link to main.css`);
|
||||||
|
|
||||||
// 3. Internal Link Integrity (No 404s!)
|
// 3. Internal Link Integrity (No 404s!)
|
||||||
const internalLinks = [...content.matchAll(/href="(\/[^"#?]*)"/g)].map((m) => m[1]);
|
const internalLinks = [...content.matchAll(/href="(\/[^"#?]*)"/g)].map((m) => m[1]);
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { postComparator, isPostPinned, getPostTimestamp } from "../src/_utils/sort-posts.js";
|
||||||
|
|
||||||
|
test("Post Sorting: isPostPinned detection", () => {
|
||||||
|
assert.equal(isPostPinned({ data: { pinned: true } }), true);
|
||||||
|
assert.equal(isPostPinned({ data: { featured: true } }), true);
|
||||||
|
assert.equal(isPostPinned({ data: { pinned: false } }), false);
|
||||||
|
assert.equal(isPostPinned({ data: { featured: false } }), false);
|
||||||
|
assert.equal(isPostPinned({ data: { pinned: null } }), false);
|
||||||
|
assert.equal(isPostPinned({ data: {} }), false);
|
||||||
|
assert.equal(isPostPinned({}), false);
|
||||||
|
assert.equal(isPostPinned(null), false);
|
||||||
|
assert.equal(isPostPinned(undefined), false);
|
||||||
|
assert.equal(isPostPinned("invalid"), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Post Sorting: getPostTimestamp extraction and fallback", () => {
|
||||||
|
const d = new Date("2026-09-01T00:00:00Z");
|
||||||
|
assert.equal(getPostTimestamp({ date: d }), d.getTime());
|
||||||
|
assert.equal(getPostTimestamp({ data: { date: "2026-09-01" } }), new Date("2026-09-01").getTime());
|
||||||
|
assert.equal(getPostTimestamp({ data: { created: "2026-09-01" } }), new Date("2026-09-01").getTime());
|
||||||
|
assert.equal(getPostTimestamp({ date: "not-a-date" }), 0);
|
||||||
|
assert.equal(getPostTimestamp({}), 0);
|
||||||
|
assert.equal(getPostTimestamp(null), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Post Sorting: standard chronological sort (unpinned posts)", () => {
|
||||||
|
const postNew = {
|
||||||
|
data: { title: "Newer Post" },
|
||||||
|
date: new Date("2026-09-10")
|
||||||
|
};
|
||||||
|
const postOld = {
|
||||||
|
data: { title: "Older Post" },
|
||||||
|
date: new Date("2026-08-01")
|
||||||
|
};
|
||||||
|
|
||||||
|
const sorted = [postOld, postNew].sort(postComparator);
|
||||||
|
assert.deepEqual(sorted.map((p) => p.data.title), ["Newer Post", "Older Post"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Post Sorting: pinned post forces top position over newer unpinned post", () => {
|
||||||
|
const newerUnpinned = {
|
||||||
|
data: { title: "Newer Unpinned", pinned: false },
|
||||||
|
date: new Date("2026-09-20")
|
||||||
|
};
|
||||||
|
const olderPinned = {
|
||||||
|
data: { title: "Older Pinned", pinned: true },
|
||||||
|
date: new Date("2026-07-01")
|
||||||
|
};
|
||||||
|
|
||||||
|
const sorted = [newerUnpinned, olderPinned].sort(postComparator);
|
||||||
|
assert.equal(sorted[0].data.title, "Older Pinned");
|
||||||
|
assert.equal(sorted[1].data.title, "Newer Unpinned");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Post Sorting: featured post forces top position over newer unpinned post", () => {
|
||||||
|
const newerRegular = {
|
||||||
|
data: { title: "Fresh Article" },
|
||||||
|
date: new Date("2026-09-22")
|
||||||
|
};
|
||||||
|
const olderFeatured = {
|
||||||
|
data: { title: "Featured Pillar", featured: true },
|
||||||
|
date: new Date("2026-06-15")
|
||||||
|
};
|
||||||
|
|
||||||
|
const sorted = [newerRegular, olderFeatured].sort(postComparator);
|
||||||
|
assert.equal(sorted[0].data.title, "Featured Pillar");
|
||||||
|
assert.equal(sorted[1].data.title, "Fresh Article");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Post Sorting: multiple pinned posts sort among themselves chronologically", () => {
|
||||||
|
const olderPinned = {
|
||||||
|
data: { title: "Older Pinned", pinned: true },
|
||||||
|
date: new Date("2026-05-01")
|
||||||
|
};
|
||||||
|
const newerPinned = {
|
||||||
|
data: { title: "Newer Pinned", featured: true },
|
||||||
|
date: new Date("2026-08-15")
|
||||||
|
};
|
||||||
|
const freshUnpinned = {
|
||||||
|
data: { title: "Fresh Regular" },
|
||||||
|
date: new Date("2026-09-01")
|
||||||
|
};
|
||||||
|
|
||||||
|
const list = [olderPinned, freshUnpinned, newerPinned];
|
||||||
|
const sorted = list.sort(postComparator);
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
sorted.map((p) => p.data.title),
|
||||||
|
["Newer Pinned", "Older Pinned", "Fresh Regular"]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Post Sorting: adversarial robustness against missing data or equal dates", () => {
|
||||||
|
const postNoData = { date: new Date("2026-09-01") };
|
||||||
|
const postMalformedDate = { data: { title: "Malformed", date: "invalid-date" } };
|
||||||
|
const postIdenticalDate1 = { data: { title: "Post A" }, date: new Date("2026-09-01") };
|
||||||
|
const postIdenticalDate2 = { data: { title: "Post B" }, date: new Date("2026-09-01") };
|
||||||
|
|
||||||
|
assert.doesNotThrow(() => {
|
||||||
|
[postNoData, postMalformedDate, postIdenticalDate1, postIdenticalDate2].sort(postComparator);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user