Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/app/tools/olf/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { Metadata } from "next";
import { MetaHelper } from "@/helpers/MetaHelper";

// The page itself is a client component, so its metadata (including the canonical) lives here.
// The page itself is a client component, so its metadata (including the canonical) and its h1 live here;
// the visible title comes from PageHeader, which is not a heading.
export const metadata: Metadata = MetaHelper.getMetaData("Open Lesson Format Builder - Lessons.church", "Build and preview lessons in the Open Lesson Format, a free, open JSON format for sharing church curriculum between apps.", undefined, undefined, "/tools/olf");

export default function Layout({ children }: { children: React.ReactNode }) { return children; }
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<h1 style={{ position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)", whiteSpace: "nowrap" }}>Open Lesson Format Builder</h1>
{children}
</>
);
}
12 changes: 10 additions & 2 deletions src/app/tools/oll/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { Metadata } from "next";
import { MetaHelper } from "@/helpers/MetaHelper";

// The page itself is a client component, so its metadata (including the canonical) lives here.
// The page itself is a client component, so its metadata (including the canonical) and its h1 live here;
// the visible title comes from PageHeader, which is not a heading.
export const metadata: Metadata = MetaHelper.getMetaData("Open Lesson List Builder - Lessons.church", "Build lesson lists in the Open Lesson List format to share free church curriculum programs, studies and lessons between apps.", undefined, undefined, "/tools/oll");

export default function Layout({ children }: { children: React.ReactNode }) { return children; }
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<h1 style={{ position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)", whiteSpace: "nowrap" }}>Open Lesson List Builder</h1>
{children}
</>
);
}
18 changes: 16 additions & 2 deletions tests/seo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ const studyPath = `${programPath}/${SEED.STUDIES.GENESIS.slug}`;
const lessonPath = `${studyPath}/${SEED.LESSONS.CREATION.slug}`;

const linkHref = (html: string, rel: string) => [...html.matchAll(new RegExp(`<link[^>]*rel="${rel}"[^>]*>`, "g"))].map((m) => /href="([^"]*)"/.exec(m[0])?.[1]);
const jsonLd = (html: string) => [...html.matchAll(/<script type="application\/ld\+json">([^<]*)<\/script>/g)].map((m) => JSON.parse(m[1]));
// Each script is either one node or an @graph of nodes; flatten to the nodes.
const jsonLd = (html: string) => [...html.matchAll(/<script type="application\/ld\+json">([^<]*)<\/script>/g)].flatMap((m) => { const data = JSON.parse(m[1]); return data["@graph"] ?? [data]; });
const metaContent = (html: string, property: string) => [...html.matchAll(new RegExp(`<meta[^>]*property="${property}"[^>]*>`, "g"))].map((m) => /content="([^"]*)"/.exec(m[0])?.[1]);

test.describe("SEO", () => {
Expand Down Expand Up @@ -50,7 +51,7 @@ test.describe("SEO", () => {
const html = await (await request.get(lessonPath)).text();
const lesson = jsonLd(html).find((d) => d["@type"] === "LearningResource");
expect(lesson).toBeTruthy();
expect(html).toContain(`<title>${SEED.PROGRAMS.OT.name}: ${lesson.name} - `);
expect(html).toContain(`<title>${lesson.name} | Free ${SEED.PROGRAMS.OT.name} Lesson</title>`);
expect(lesson.url).toBe(SITE + lessonPath);
expect(lesson.isPartOf).toMatchObject({ "@type": "Course", url: SITE + studyPath, isPartOf: { "@type": "Course", url: SITE + programPath } });
});
Expand Down Expand Up @@ -86,6 +87,19 @@ test.describe("SEO", () => {
});
}

// Both are in the sitemap but render in the browser, so these tags and the h1 come from their layouts.
for (const [path, title] of [["/tools/olf", "Open Lesson Format Builder"], ["/tools/oll", "Open Lesson List Builder"]]) {
test(`${path} describes itself`, async ({ request }) => {
const html = await (await request.get(path)).text();
expect(html).toContain(`<title>${title} - Lessons.church</title>`);
expect(/<meta name="description" content="([^"]{50,})"/.test(html)).toBe(true);
expect(linkHref(html, "canonical")).toEqual([SITE + path]);
expect(metaContent(html, "og:url")).toEqual([SITE + path]);
expect([...html.matchAll(/<h1[\s>]/g)].length).toBe(1);
expect(html).toMatch(new RegExp(`<h1[^>]*>${title}</h1>`));
});
}

test("search results are noindex", async ({ request }) => {
const html = await (await request.get("/search?q=noah")).text();
expect(html).toMatch(/<meta name="robots" content="noindex/);
Expand Down
Loading