-
+
{post.title}
-
+
{hasNonNullContent(post.author) && (
<>
-
+
{props.localization.attribution}
{reduceAuthors(post.author)}
>
)}
-
{showExcerpt && (post.excerpt != "") && (
@@ -63,17 +71,3 @@ function authorElement(author: string) {
const authorUrl = `/author/${author.toLowerCase().replace(/\s+/g, "-")}`;
return
{author};
}
-
-function tags(tags: string[]) {
- return tags.map((tag) => {
- const url = `/archive/${tag}`;
- return (
-
- {tag}
-
- );
- });
-}
diff --git a/src/components/TagLink.tsx b/src/components/TagLink.tsx
new file mode 100644
index 0000000..9d835d2
--- /dev/null
+++ b/src/components/TagLink.tsx
@@ -0,0 +1,11 @@
+export default function TagLink({ tag }: { tag: string }) {
+ return (
+
+ {tag}
+
+ );
+}
diff --git a/src/components/Tags.tsx b/src/components/Tags.tsx
new file mode 100644
index 0000000..202a468
--- /dev/null
+++ b/src/components/Tags.tsx
@@ -0,0 +1,10 @@
+import TagLink from "./TagLink.tsx";
+
+export default function Tags({ tags, id }: { tags: string[]; id?: string }) {
+ return (
+
+ {tags.some((x) => x != null) &&
+ tags.map((tag, index) => )}
+
+ );
+}
diff --git a/src/islands/NavigationBar.tsx b/src/islands/NavigationBar.tsx
index 77ddd60..652fa21 100644
--- a/src/islands/NavigationBar.tsx
+++ b/src/islands/NavigationBar.tsx
@@ -1,75 +1,67 @@
import { BlogOptions } from "../plugin/blog.ts";
-import { useSignal } from "https://esm.sh/*@preact/signals@1.2.2";
+import { useSignal } from "@preact/signals";
+import ThemeToggle from "./ThemeToggle.tsx";
export default function NavigationBar(
- props: { active: string; class?: string; options: BlogOptions },
+ props: { class?: string; options: BlogOptions },
) {
- const isHome = props.active == "/";
const isOpen = useSignal(false);
return (
-
-
-
- {props.options.title}
-
-
+
Primary Navigation
+
+
);
}
diff --git a/src/islands/ThemeToggle.tsx b/src/islands/ThemeToggle.tsx
new file mode 100644
index 0000000..72613e6
--- /dev/null
+++ b/src/islands/ThemeToggle.tsx
@@ -0,0 +1,61 @@
+import { useSignal } from "@preact/signals";
+
+const sun = (
+
+);
+
+const moon = (
+
+);
+
+export default function ThemeToggle() {
+ const currentTheme = localStorage.getItem("theme") ?? "light";
+ const isDarkMode = useSignal(currentTheme === "dark");
+
+ const toggleTheme = () => {
+ isDarkMode.value = !isDarkMode.value;
+ const theme = isDarkMode.value ? "dark" : "light";
+
+ document.documentElement.classList.toggle("dark", isDarkMode.value);
+ const markdownBody = document.getElementById("markdown-body");
+ if (markdownBody) {
+ markdownBody.setAttribute(
+ "data-color-mode",
+ isDarkMode.value ? "dark" : "light",
+ );
+ }
+
+ document.cookie = `theme=${theme};path=/;max-age=31536000;SameSite=Lax`;
+ localStorage.setItem("theme", theme);
+ };
+
+ return (
+
+ );
+}
diff --git a/src/plugin/blog.ts b/src/plugin/blog.ts
index 7fc3af0..84571ec 100644
--- a/src/plugin/blog.ts
+++ b/src/plugin/blog.ts
@@ -130,7 +130,11 @@ export function blogPlugin(
}],
islands: {
baseLocation: import.meta.url,
- paths: ["../islands/NavigationBar.tsx", "../islands/Disqus.tsx"],
+ paths: [
+ "../islands/NavigationBar.tsx",
+ "../islands/Disqus.tsx",
+ "../islands/ThemeToggle.tsx",
+ ],
},
};
}
diff --git a/src/routes/_app.tsx b/src/routes/_app.tsx
index aa0f678..a8e2955 100644
--- a/src/routes/_app.tsx
+++ b/src/routes/_app.tsx
@@ -1,22 +1,23 @@
-import { PageProps } from "../../deps.ts";
+import { FreshContext } from "../../deps.ts";
import Header from "../components/Header.tsx";
import { BlogOptions } from "../plugin/blog.ts";
+import { themeFromRequest } from "../utils/theme.ts";
export function AppBuilder(options: BlogOptions) {
- return (props: PageProps) => {
- const { Component, route } = props;
+ // deno-lint-ignore require-await
+ return async (req: Request, ctx: FreshContext) => {
+ const themeClass = themeFromRequest(req);
+
+ const { Component } = ctx;
return (
-
+
-
-
-
+
+
+
diff --git a/src/routes/archive/index.tsx b/src/routes/archive/index.tsx
index 5989898..c933e4a 100644
--- a/src/routes/archive/index.tsx
+++ b/src/routes/archive/index.tsx
@@ -3,6 +3,8 @@ import { Post } from "../../utils/posts.ts";
import PostList from "../../components/PostList.tsx";
import { BlogState } from "../_middleware.ts";
import { Localization, PageOptions } from "../../plugin/blog.ts";
+import TagLink from "../../components/TagLink.tsx";
+import Tags from "../../components/Tags.tsx";
export const handler: Handlers = {
GET(_req, ctx) {
@@ -36,18 +38,8 @@ export function createArchivePage(
{finalTitle}
-
-
- {allTags.some((x) => x != null) && allTags.map((tag, index) => (
-
- {tag}
-
- ))}
-
+
+
= {
+ async GET(req, ctx) {
+ const theme = themeFromRequest(req);
-export const handler: Handlers = {
- async GET(_req, ctx) {
const posts = ctx.state.context.posts;
const post = posts.find((x) => x.slug === ctx.params.slug);
if (!post) {
@@ -34,7 +38,7 @@ export const handler: Handlers = {
.map((block) => block.code.rich_text[0].plain_text);
post.content = codeContent[0];
}
- return ctx.render(post!);
+ return ctx.render({ theme, post: post! });
},
};
@@ -44,8 +48,8 @@ export function createPostPage(
comments?: DisqusOptions,
options?: PageOptions,
) {
- return function PostPage(props: PageProps) {
- const post = props.data;
+ return function PostPage(props: PageProps<{ theme: string; post: Post }>) {
+ const post = props.data.post;
class CustomRenderer extends Renderer {
list(body: string, ordered: boolean): string {
const type = ordered ? "list-decimal" : "list-disc";
@@ -61,33 +65,45 @@ export function createPostPage(
},
});
return (
- <>
+
{options?.titleOverride || title} — {post.title}
-
-
-
{post.title}
-
- {new Date(post.date).toLocaleDateString("en-us", {
- year: "numeric",
- month: "long",
- day: "numeric",
- })}
-
-
- {comments && comments.source === "disqus" && (
-
+
+
- )}
-
+
+ Next Post Previous Post Navigation
+
+
- >
+
);
};
}
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index a61bf17..af27e22 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -57,7 +57,7 @@ export function createBlogIndexPage(
{options?.titleOverride || title}
-
+
{
+ await page.goto(`${address}/blog/markdown-test`, { waitUntil: "load" });
+ await page.setViewport({ width: 1920, height: 1080 });
+
+ const getComputedStyle = (
+ selector: string,
+ property: keyof CSSStyleDeclaration,
+ ) => {
+ return page.evaluate(
+ (selector, property) => {
+ const element = document.querySelector(selector);
+ if (!element) {
+ return null;
+ }
+ const style = globalThis.getComputedStyle(element);
+ return style[property];
+ },
+ selector,
+ property,
+ );
+ };
+
+ await t.step("page header color and theme toggle", async () => {
+ let headerColor = await getComputedStyle(
+ "#post\\:markdown-test > a > h3",
+ "color",
+ );
+ assertEquals(
+ headerColor,
+ "rgb(0, 0, 0)",
+ "header color should be black",
+ );
+ await page.click(
+ "body > header > div > nav > ul > li:nth-child(3) > button",
+ );
+ headerColor = await getComputedStyle(
+ "#post\\:markdown-test > a > h3",
+ "color",
+ );
+ assertEquals(
+ headerColor,
+ "rgb(255, 255, 255)",
+ "header color should be white",
+ );
+ });
+ },
+ {},
+ );
+ },
+});
diff --git a/tests/styles_fixture/blog.config.ts b/tests/styles_fixture/blog.config.ts
new file mode 100644
index 0000000..4620560
--- /dev/null
+++ b/tests/styles_fixture/blog.config.ts
@@ -0,0 +1,11 @@
+import { BlogOptions } from "../../mod.ts";
+
+export default {
+ title: "Demo Blog",
+ navbarItems: {
+ Home: "/",
+ Archive: "/archive",
+ },
+ rootPath: import.meta.url,
+ postsPerPage: 5,
+} satisfies BlogOptions;
diff --git a/tests/styles_fixture/deno.json b/tests/styles_fixture/deno.json
new file mode 100644
index 0000000..00fec29
--- /dev/null
+++ b/tests/styles_fixture/deno.json
@@ -0,0 +1,23 @@
+{
+ "lock": false,
+ "tasks": {
+ "start": "deno run -A --watch=static/,routes/ dev.ts",
+ "update": "deno run -A -r https://fresh.deno.dev/update .",
+ "build": "deno run -A dev.ts build",
+ "preview": "deno run -A main.ts"
+ },
+ "imports": {
+ "$fresh/": "https://raw.githubusercontent.com/denoland/fresh/844370cadd1ed28fd76f796c2afc1e2411bfc425/",
+ "preact": "https://esm.sh/preact@10.19.3",
+ "preact/": "https://esm.sh/preact@10.19.3/",
+ "@preact/signals": "https://esm.sh/*@preact/signals@1.2.2",
+ "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.1",
+ "tailwindcss": "npm:tailwindcss@3.4.1",
+ "tailwindcss/": "npm:/tailwindcss@3.4.1/",
+ "tailwindcss/plugin": "npm:/tailwindcss@3.4.1/plugin.js",
+ "$std/": "https://deno.land/std@0.214.0/"
+ },
+ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "preact" },
+ "lint": { "rules": { "tags": ["fresh", "recommended"] } },
+ "exclude": ["**/_fresh/*"]
+}
diff --git a/tests/styles_fixture/dev.ts b/tests/styles_fixture/dev.ts
new file mode 100755
index 0000000..ae73946
--- /dev/null
+++ b/tests/styles_fixture/dev.ts
@@ -0,0 +1,8 @@
+#!/usr/bin/env -S deno run -A --watch=static/,routes/
+
+import dev from "$fresh/dev.ts";
+import config from "./fresh.config.ts";
+
+import "$std/dotenv/load.ts";
+
+await dev(import.meta.url, "./main.ts", config);
diff --git a/tests/styles_fixture/fresh.config.ts b/tests/styles_fixture/fresh.config.ts
new file mode 100644
index 0000000..d66c088
--- /dev/null
+++ b/tests/styles_fixture/fresh.config.ts
@@ -0,0 +1,9 @@
+import { defineConfig } from "$fresh/server.ts";
+import tailwind from "$fresh/plugins/tailwind.ts";
+
+import { blogPlugin } from "../../src/plugin/blog.ts";
+import blogConfig from "./blog.config.ts";
+
+export default defineConfig({
+ plugins: [tailwind(), blogPlugin(blogConfig)],
+});
diff --git a/tests/styles_fixture/fresh.gen.ts b/tests/styles_fixture/fresh.gen.ts
new file mode 100644
index 0000000..003601d
--- /dev/null
+++ b/tests/styles_fixture/fresh.gen.ts
@@ -0,0 +1,17 @@
+// DO NOT EDIT. This file is generated by Fresh.
+// This file SHOULD be checked into source version control.
+// This file is automatically updated during development when running `dev.ts`.
+
+import * as $_404 from "./routes/_404.tsx";
+
+import { type Manifest } from "$fresh/server.ts";
+
+const manifest = {
+ routes: {
+ "./routes/_404.tsx": $_404,
+ },
+ islands: {},
+ baseUrl: import.meta.url,
+} satisfies Manifest;
+
+export default manifest;
diff --git a/tests/styles_fixture/main.ts b/tests/styles_fixture/main.ts
new file mode 100644
index 0000000..675f529
--- /dev/null
+++ b/tests/styles_fixture/main.ts
@@ -0,0 +1,13 @@
+///
+///
+///
+///
+///
+
+import "$std/dotenv/load.ts";
+
+import { start } from "$fresh/server.ts";
+import manifest from "./fresh.gen.ts";
+import config from "./fresh.config.ts";
+
+await start(manifest, config);
diff --git a/tests/styles_fixture/options.ts b/tests/styles_fixture/options.ts
new file mode 100644
index 0000000..8d1b398
--- /dev/null
+++ b/tests/styles_fixture/options.ts
@@ -0,0 +1,11 @@
+import { FreshOptions } from "$fresh/server.ts";
+
+export default {
+ async render(_ctx, render) {
+ await new Promise((r) => r());
+ const body = render();
+ if (typeof body !== "string") {
+ throw new Error("body is missing");
+ }
+ },
+} as FreshOptions;
diff --git a/tests/styles_fixture/posts/markdown-test.md b/tests/styles_fixture/posts/markdown-test.md
new file mode 100644
index 0000000..d9fce50
--- /dev/null
+++ b/tests/styles_fixture/posts/markdown-test.md
@@ -0,0 +1,27 @@
+---
+title: "Markdown Test"
+date: 2023-8-14 04:39
+author: Some Author
+tags:
+ - example
+---
+
+This should actually, you know, work.
+
+
+
+# Big Stuff
+
+hello
+
+## Not quite as big
+
+~~hey again~~
+
+- a
+- b
+- c
+
+1. a
+2. b
+3. c
diff --git a/tests/styles_fixture/routes/_404.tsx b/tests/styles_fixture/routes/_404.tsx
new file mode 100644
index 0000000..d286378
--- /dev/null
+++ b/tests/styles_fixture/routes/_404.tsx
@@ -0,0 +1,3 @@
+export default function Error404() {
+ return "Not found.";
+}
diff --git a/tests/styles_fixture/static/favicon.ico b/tests/styles_fixture/static/favicon.ico
new file mode 100644
index 0000000..1cfaaa2
Binary files /dev/null and b/tests/styles_fixture/static/favicon.ico differ
diff --git a/tests/styles_fixture/static/styles.css b/tests/styles_fixture/static/styles.css
new file mode 100644
index 0000000..d3f3944
--- /dev/null
+++ b/tests/styles_fixture/static/styles.css
@@ -0,0 +1,10 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ #markdown-body[data-color-mode="dark"] {
+ --color-canvas-default: #000000;
+ --color-fg-default: #ffffff;
+ }
+}
diff --git a/tests/styles_fixture/tailwind.config.ts b/tests/styles_fixture/tailwind.config.ts
new file mode 100644
index 0000000..788f3db
--- /dev/null
+++ b/tests/styles_fixture/tailwind.config.ts
@@ -0,0 +1,29 @@
+import { type Config } from "tailwindcss";
+import { safelist } from "../../src/safelist.ts";
+import colors from "tailwindcss/colors.js";
+
+export default {
+ darkMode: "class",
+ content: [
+ "{routes,islands,components}/**/*.{ts,tsx}",
+ ],
+ theme: {
+ extend: {
+ colors: {
+ light: {
+ background: colors.white,
+ foreground: colors.black,
+ mutedBackground: colors.gray[200],
+ mutedForeground: colors.gray[700],
+ },
+ dark: {
+ background: colors.black,
+ foreground: colors.white,
+ mutedBackground: colors.pink[700],
+ mutedForeground: colors.gray[400],
+ },
+ },
+ },
+ },
+ safelist: safelist,
+} satisfies Config;
diff --git a/tests/test_utils.ts b/tests/test_utils.ts
index c4a9acc..ee561bf 100644
--- a/tests/test_utils.ts
+++ b/tests/test_utils.ts
@@ -47,6 +47,7 @@ export async function withPageName(
await fn(page, address);
} catch (err) {
console.error("Error in test function:", err);
+ throw err;
} finally {
await browser.close();
}
diff --git a/tests/tests_parameterized.ts b/tests/tests_parameterized.ts
index c9e174d..aa93ae7 100644
--- a/tests/tests_parameterized.ts
+++ b/tests/tests_parameterized.ts
@@ -113,7 +113,9 @@ export function parameterizedTests(config: BlogOptions) {
});
await t.step("archive page has alphabetically sorted links", () => {
- const tagLinks = Array.from(doc.querySelectorAll('a[id^="tag-link-"]'));
+ const tagLinks = Array.from(
+ doc.querySelectorAll('#allTags a[id^="tag-link-"]'),
+ );
const tags = tagLinks.map((link) => link.textContent);
const sortedTags = [...tags].sort();
assertEquals(tags, sortedTags);