Skip to content

Commit a50dadf

Browse files
authored
Merge pull request #34 from willwearing/feat/landing-page-docs-seo
feat: agent-first landing page, docs, SEO
2 parents 283f5b1 + 2fcb10f commit a50dadf

57 files changed

Lines changed: 4092 additions & 429 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/web/next.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { NextConfig } from "next";
22

33
const nextConfig: NextConfig = {
4-
transpilePackages: ["@graspful/shared"],
4+
transpilePackages: ["@niche-audio-prep/shared"],
55
};
66

77
export default nextConfig;

apps/web/package.json

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,34 @@
11
{
2-
"name": "@graspful/web",
2+
"name": "@niche-audio-prep/web",
33
"version": "0.1.0",
44
"private": true,
55
"scripts": {
66
"dev": "next dev --port 3001 --turbopack",
77
"build": "next build",
88
"start": "next start",
9-
"lint": "eslint",
10-
"test": "bun x vitest run",
11-
"test:watch": "bun x vitest",
12-
"test:e2e": "bun x playwright test",
13-
"test:e2e:list": "bun x playwright test --list"
9+
"lint": "eslint"
1410
},
1511
"dependencies": {
1612
"@base-ui/react": "^1.2.0",
17-
"@supabase/ssr": "^0.9.0",
18-
"@supabase/supabase-js": "^2.99.0",
19-
"@xyflow/react": "^12.10.1",
2013
"class-variance-authority": "^0.7.1",
2114
"clsx": "^2.1.1",
2215
"lucide-react": "^0.577.0",
2316
"next": "16.1.6",
24-
"posthog-js": "^1.360.0",
25-
"posthog-node": "^5.28.0",
2617
"react": "19.2.3",
2718
"react-dom": "19.2.3",
28-
"recharts": "^3.8.0",
2919
"shadcn": "^4.0.2",
3020
"tailwind-merge": "^3.5.0",
3121
"tw-animate-css": "^1.4.0"
3222
},
3323
"devDependencies": {
34-
"@playwright/test": "^1.58.2",
3524
"@tailwindcss/postcss": "^4",
36-
"@testing-library/jest-dom": "^6.9.1",
37-
"@testing-library/react": "^16.3.2",
3825
"@types/node": "^20",
3926
"@types/react": "^19",
4027
"@types/react-dom": "^19",
41-
"@vitejs/plugin-react": "^5.1.4",
4228
"eslint": "^9",
4329
"eslint-config-next": "16.1.6",
44-
"fake-indexeddb": "^6.2.5",
45-
"jsdom": "^28.1.0",
4630
"tailwindcss": "^4",
47-
"typescript": "^5",
48-
"vitest": "^4.0.18"
31+
"typescript": "^5"
4932
},
5033
"ignoreScripts": [
5134
"sharp",

apps/web/public/llms.txt

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,21 @@
22

33
> Graspful is the fastest way to create adaptive learning courses. Define a course as YAML, import it via CLI or MCP, and get a live product with a landing page, knowledge graph, spaced repetition, and billing — in minutes.
44

5+
## Links
6+
7+
- Website: https://graspful.com
8+
- AI Agents: https://graspful.com/agents
9+
- Documentation: https://graspful.com/docs
10+
- Quickstart: https://graspful.com/docs/quickstart
11+
- Pricing: https://graspful.com/pricing
12+
- Sign Up: https://graspful.com/sign-up
13+
514
## Getting Started
615

7-
- [CLI Quickstart](https://graspful.com/docs/cli-quickstart): Install and create your first course
8-
- [Course YAML Schema](https://graspful.com/docs/course-yaml-schema): Full reference for course YAML format
9-
- [Brand YAML Schema](https://graspful.com/docs/brand-yaml-schema): Landing page, theme, pricing, SEO
10-
- [Content Authoring Guide](https://graspful.com/docs/adding-a-course): Step-by-step course creation workflow
16+
- [Quickstart](https://graspful.com/docs/quickstart): Install and create your first course
17+
- [Course YAML Schema](https://graspful.com/docs/course-schema): Full reference for course YAML format
18+
- [Brand YAML Schema](https://graspful.com/docs/brand-schema): Landing page, theme, pricing, SEO
19+
- [MCP Server](https://graspful.com/docs/mcp): Set up MCP for Claude Code, Cursor, Codex
1120

1221
## Agent Instructions
1322

apps/web/src/__tests__/components/lesson-flow.test.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ describe("LessonFlow", () => {
135135
fireEvent.click(screen.getByRole("button", { name: /continue/i }));
136136
fireEvent.click(screen.getByRole("button", { name: /continue/i }));
137137

138-
fireEvent.click(screen.getByRole("button", { name: "Heat" }));
138+
fireEvent.click(screen.getByRole("radio", { name: "Heat" }));
139139
fireEvent.click(screen.getByRole("button", { name: /submit answer/i }));
140140
await waitFor(() => {
141141
expect(screen.getByText("Practice complete")).toBeTruthy();
@@ -154,7 +154,7 @@ describe("LessonFlow", () => {
154154
// KP1: instruction -> worked example -> practice -> answer -> continue
155155
fireEvent.click(screen.getByRole("button", { name: /continue/i }));
156156
fireEvent.click(screen.getByRole("button", { name: /continue/i }));
157-
fireEvent.click(screen.getByRole("button", { name: "Heat" }));
157+
fireEvent.click(screen.getByRole("radio", { name: "Heat" }));
158158
fireEvent.click(screen.getByRole("button", { name: /submit answer/i }));
159159
await waitFor(() => {
160160
expect(screen.getByText("Practice complete")).toBeTruthy();
@@ -164,7 +164,7 @@ describe("LessonFlow", () => {
164164
// KP2: instruction -> worked example -> practice -> answer
165165
fireEvent.click(screen.getByRole("button", { name: /continue/i }));
166166
fireEvent.click(screen.getByRole("button", { name: /continue/i }));
167-
fireEvent.click(screen.getByRole("button", { name: /all surfaces igniting in a room/i }));
167+
fireEvent.click(screen.getByRole("radio", { name: /all surfaces igniting in a room/i }));
168168
fireEvent.click(screen.getByRole("button", { name: /submit answer/i }));
169169
await waitFor(() => {
170170
expect(screen.getByRole("button", { name: /complete lesson/i })).toBeTruthy();
@@ -182,7 +182,7 @@ describe("LessonFlow", () => {
182182
// KP1: instruction -> worked example -> practice -> answer -> continue
183183
fireEvent.click(screen.getByRole("button", { name: /continue/i }));
184184
fireEvent.click(screen.getByRole("button", { name: /continue/i }));
185-
fireEvent.click(screen.getByRole("button", { name: "Heat" }));
185+
fireEvent.click(screen.getByRole("radio", { name: "Heat" }));
186186
fireEvent.click(screen.getByRole("button", { name: /submit answer/i }));
187187
await waitFor(() => {
188188
expect(screen.getByText("Practice complete")).toBeTruthy();
@@ -192,7 +192,7 @@ describe("LessonFlow", () => {
192192
// KP2: instruction -> worked example -> practice -> answer -> complete
193193
fireEvent.click(screen.getByRole("button", { name: /continue/i }));
194194
fireEvent.click(screen.getByRole("button", { name: /continue/i }));
195-
fireEvent.click(screen.getByRole("button", { name: /all surfaces igniting in a room/i }));
195+
fireEvent.click(screen.getByRole("radio", { name: /all surfaces igniting in a room/i }));
196196
fireEvent.click(screen.getByRole("button", { name: /submit answer/i }));
197197
await waitFor(() => {
198198
expect(screen.getByRole("button", { name: /complete lesson/i })).toBeTruthy();

apps/web/src/app/(app)/academy/[academyId]/diagnostic/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export default async function AcademyDiagnosticPage({
3737
let startData: any = null;
3838
let errorMessage: string | null = null;
3939
try {
40-
startData = await apiFetch<any>(`${basePath}/diagnostic/start`);
40+
startData = await apiFetch<any>(`${basePath}/diagnostic/start`, { method: 'POST' });
4141
} catch (err) {
4242
if (err instanceof ApiError && err.statusCode === 400) {
4343
errorMessage = "This academy doesn't have diagnostic content yet. Check back soon.";

apps/web/src/app/(app)/academy/[academyId]/page.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,15 @@ export default async function AcademyPage({
153153
) : null}
154154

155155
{/* Knowledge graph */}
156-
<div className="mb-8">
157-
<KnowledgeGraphSection
158-
orgSlug={brand.orgSlug}
159-
courseId={courses[0]?.id ?? ""}
160-
academyId={academyId}
161-
/>
162-
</div>
156+
{courses[0]?.id && (
157+
<div className="mb-8">
158+
<KnowledgeGraphSection
159+
orgSlug={brand.orgSlug}
160+
courseId={courses[0].id}
161+
academyId={academyId}
162+
/>
163+
</div>
164+
)}
163165

164166
<h2 className="mb-4 text-xl font-semibold text-foreground">Courses</h2>
165167

@@ -179,7 +181,6 @@ export default async function AcademyPage({
179181
href={getCourseBrowseHref(course.id)}
180182
name={course.name}
181183
description={course.description}
182-
orgId={course.orgId}
183184
completionPercent={courseProfile?.completionPercent ?? 0}
184185
totalConcepts={courseProfile?.totalConcepts ?? 0}
185186
mastered={courseProfile?.mastered ?? 0}

apps/web/src/app/(app)/dashboard/page.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,6 @@ export default async function DashboardPage() {
262262
courseId={course.id}
263263
name={course.name}
264264
description={course.description}
265-
orgId={course.orgId}
266265
completionPercent={profile?.completionPercent ?? 0}
267266
totalConcepts={profile?.totalConcepts ?? 0}
268267
mastered={profile?.mastered ?? 0}

apps/web/src/app/(app)/diagnostic/[courseId]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export default async function DiagnosticPage({
3838
let startData: any = null;
3939
let errorMessage: string | null = null;
4040
try {
41-
startData = await apiFetch<any>(`${basePath}/diagnostic/start`);
41+
startData = await apiFetch<any>(`${basePath}/diagnostic/start`, { method: 'POST' });
4242
} catch (err) {
4343
if (err instanceof ApiError && err.statusCode === 400) {
4444
errorMessage = "This course doesn't have diagnostic content yet. Check back soon.";

apps/web/src/app/(app)/study/[courseId]/loading.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Skeleton } from "@/components/ui/skeleton";
22

33
export default function StudyLoading() {
44
return (
5-
<div className="mx-auto max-w-4xl px-4 py-8 md:px-8 flex items-center justify-center py-12">
5+
<div className="mx-auto max-w-4xl px-4 md:px-8 flex items-center justify-center py-12">
66
<Skeleton className="h-6 w-48" />
77
</div>
88
);

apps/web/src/app/(app)/study/[courseId]/quiz/page.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,15 @@ export default async function QuizPage({
2222
const brand = await resolvePageBrand();
2323
const orgSlug = brand.orgSlug;
2424

25-
const quizData = await apiFetch<any>(
26-
`/orgs/${orgSlug}/courses/${courseId}/quizzes/generate`,
27-
{ method: "POST" }
28-
);
25+
let quizData: any;
26+
try {
27+
quizData = await apiFetch<any>(
28+
`/orgs/${orgSlug}/courses/${courseId}/quizzes/generate`,
29+
{ method: "POST" }
30+
);
31+
} catch {
32+
redirect(`/study/${courseId}`);
33+
}
2934

3035
return (
3136
<div className="mx-auto max-w-3xl px-4 py-8 md:px-8">

0 commit comments

Comments
 (0)