Skip to content

Commit 8c44cbe

Browse files
authored
feat(astro): support lessons without parts or chapters (#374)
1 parent 5a2a64f commit 8c44cbe

38 files changed

+1450
-821
lines changed

docs/tutorialkit.dev/src/content/docs/guides/creating-content.mdx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ title: Content creation
33
description: 'Creating content in TutorialKit.'
44
---
55
import { FileTree } from '@astrojs/starlight/components';
6+
import { Tabs, TabItem } from '@astrojs/starlight/components';
67

78
From an information architecture perspective, tutorial content is divided into **parts**, which are further divided into **chapters**, each consisting of **lessons**.
89

@@ -36,6 +37,64 @@ This structure is reflected in the directory structure of your TutorialKit proje
3637

3738
Navigate into one of these folders to see another folder that represents a **chapter**. Inside the chapter folder, you will find one or more **lesson** folders.
3839

40+
You can also omit parts or chapters such that you only have lessons or only lessons and parts. Here are a few examples:
41+
42+
<Tabs>
43+
<TabItem label="Structure">
44+
```plaintext
45+
- Lesson 1: Getting started
46+
- Lesson 2: Adding pages
47+
```
48+
</TabItem>
49+
50+
<TabItem label="File tree">
51+
<FileTree>
52+
- src
53+
- content
54+
- tutorial
55+
- getting-started
56+
- _files/
57+
- _solution/
58+
- content.md
59+
- adding-pages/
60+
- meta.md
61+
- config.ts
62+
- templates/
63+
</FileTree>
64+
</TabItem>
65+
</Tabs>
66+
67+
<Tabs>
68+
<TabItem label="Structure">
69+
```plaintext
70+
- Part 1: Introduction
71+
- Lesson 1: What is Vite?
72+
- Lesson 2: Installing
73+
- …
74+
- Part 2: Project structure
75+
- …
76+
```
77+
</TabItem>
78+
79+
<TabItem label="File tree">
80+
<FileTree>
81+
- src
82+
- content
83+
- tutorial
84+
- introduction/
85+
- what-is-vite/
86+
- _files/
87+
- _solution/
88+
- content.md
89+
- installing/
90+
- project-structure/
91+
- meta.md
92+
- config.ts
93+
- templates/
94+
</FileTree>
95+
</TabItem>
96+
</Tabs>
97+
3998
## A lesson content file
4099

41100
Navigate to the `src/content/tutorial/1-basics/1-introduction/1-welcome` folder and open the `content.md` in your editor. You will see a file structured like this:

e2e/configs/lessons-in-part.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import tutorialkit from '@tutorialkit/astro';
2+
import { defineConfig } from 'astro/config';
3+
4+
export default defineConfig({
5+
devToolbar: { enabled: false },
6+
server: { port: 4332 },
7+
outDir: './dist-lessons-in-part',
8+
integrations: [tutorialkit()],
9+
srcDir: './src-custom/lessons-in-part',
10+
});

e2e/configs/lessons-in-root.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import tutorialkit from '@tutorialkit/astro';
2+
import { defineConfig } from 'astro/config';
3+
4+
export default defineConfig({
5+
devToolbar: { enabled: false },
6+
server: { port: 4331 },
7+
outDir: './dist-lessons-in-root',
8+
integrations: [tutorialkit()],
9+
srcDir: './src-custom/lessons-in-root',
10+
});

e2e/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
"preview": "astro build && astro preview",
88
"dev:override-components": "astro dev --config ./configs/override-components.ts",
99
"preview:override-components": "astro build --config ./configs/override-components.ts && astro preview --config ./configs/override-components.ts",
10+
"dev:lessons-in-root": "astro dev --config ./configs/lessons-in-root.ts",
11+
"preview:lessons-in-root": "astro build --config ./configs/lessons-in-root.ts && astro preview --config ./configs/lessons-in-root.ts",
12+
"dev:lessons-in-part": "astro dev --config ./configs/lessons-in-part.ts",
13+
"preview:lessons-in-part": "astro build --config ./configs/lessons-in-part.ts && astro preview --config ./configs/lessons-in-part.ts",
1014
"test": "playwright test",
1115
"test:ui": "pnpm run test --ui"
1216
},

e2e/playwright.config.ts

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,59 @@
11
import { defineConfig } from '@playwright/test';
22

3+
const serverOptions = {
4+
reuseExistingServer: !process.env.CI,
5+
stdout: 'ignore',
6+
stderr: 'pipe',
7+
} as const;
8+
39
export default defineConfig({
410
projects: [
511
{
612
name: 'Default',
713
testMatch: 'test/*.test.ts',
8-
testIgnore: 'test/*.override-components.test.ts',
14+
testIgnore: [
15+
'test/*.override-components.test.ts',
16+
'test/*.lessons-in-part.test.ts',
17+
'test/*.lessons-in-root.test.ts',
18+
],
919
use: { baseURL: 'http://localhost:4329' },
1020
},
1121
{
1222
name: 'Override Components',
1323
testMatch: 'test/*.override-components.test.ts',
1424
use: { baseURL: 'http://localhost:4330' },
1525
},
26+
{
27+
name: 'Lessons in root',
28+
testMatch: 'test/*.lessons-in-root.test.ts',
29+
use: { baseURL: 'http://localhost:4331' },
30+
},
31+
{
32+
name: 'Lessons in part',
33+
testMatch: 'test/*.lessons-in-part.test.ts',
34+
use: { baseURL: 'http://localhost:4332' },
35+
},
1636
],
1737
webServer: [
1838
{
1939
command: 'pnpm preview',
2040
url: 'http://localhost:4329',
21-
reuseExistingServer: !process.env.CI,
22-
stdout: 'ignore',
23-
stderr: 'pipe',
41+
...serverOptions,
2442
},
2543
{
2644
command: 'pnpm preview:override-components',
2745
url: 'http://localhost:4330',
28-
reuseExistingServer: !process.env.CI,
29-
stdout: 'ignore',
30-
stderr: 'pipe',
46+
...serverOptions,
47+
},
48+
{
49+
command: 'pnpm preview:lessons-in-root',
50+
url: 'http://localhost:4331',
51+
...serverOptions,
52+
},
53+
{
54+
command: 'pnpm preview:lessons-in-part',
55+
url: 'http://localhost:4332',
56+
...serverOptions,
3157
},
3258
],
3359
expect: {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { contentSchema } from '@tutorialkit/types';
2+
import { defineCollection } from 'astro:content';
3+
4+
const tutorial = defineCollection({
5+
type: 'content',
6+
schema: contentSchema,
7+
});
8+
9+
export const collections = { tutorial };
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
type: tutorial
3+
mainCommand: ''
4+
prepareCommands: []
5+
---
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
type: lesson
3+
title: Lesson one
4+
---
5+
6+
# Lessons in part test - Lesson one
7+
8+
Lesson in part without chapter
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
type: lesson
3+
title: Lesson two
4+
---
5+
6+
# Lessons in part test - Lesson two
7+
8+
Lesson in part without chapter
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
type: part
3+
title: 'Part one'
4+
---
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
type: lesson
3+
title: Lesson three
4+
---
5+
6+
# Lessons in part test - Lesson three
7+
8+
Lesson in chapter
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
type: lesson
3+
title: Lesson four
4+
---
5+
6+
# Lessons in part test - Lesson four
7+
8+
Lesson in chapter
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
type: chapter
3+
title: 'Chapter one'
4+
---
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
type: part
3+
title: 'Part two'
4+
---
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/// <reference path="../../.astro/types.d.ts" />
2+
/// <reference types="@tutorialkit/astro/types" />
3+
/// <reference types="astro/client" />
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { contentSchema } from '@tutorialkit/types';
2+
import { defineCollection } from 'astro:content';
3+
4+
const tutorial = defineCollection({
5+
type: 'content',
6+
schema: contentSchema,
7+
});
8+
9+
export const collections = { tutorial };
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
type: lesson
3+
title: Lesson one
4+
---
5+
6+
# Lessons in root test - Lesson one
7+
8+
Lesson in root without part
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
type: lesson
3+
title: Lesson two
4+
---
5+
6+
# Lessons in root test - Lesson two
7+
8+
Lesson in root without part
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
type: tutorial
3+
mainCommand: ''
4+
prepareCommands: []
5+
---
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/// <reference path="../../.astro/types.d.ts" />
2+
/// <reference types="@tutorialkit/astro/types" />
3+
/// <reference types="astro/client" />
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test('user can navigate between lessons using breadcrumbs', async ({ page }) => {
4+
await page.goto('/');
5+
6+
await expect(page.getByRole('heading', { level: 1, name: 'Lessons in part test - Lesson one' })).toBeVisible();
7+
await expect(page.getByText('Lesson in part without chapter')).toBeVisible();
8+
9+
// navigation select can take a while to hydrate on page load, click until responsive
10+
await expect(async () => {
11+
const button = page.getByRole('button', { name: 'Part one / Lesson one' });
12+
await button.click();
13+
await expect(page.locator('[data-state="open"]', { has: button })).toBeVisible({ timeout: 50 });
14+
}).toPass();
15+
16+
const navigation = page.getByRole('navigation');
17+
await navigation.getByRole('region', { name: 'Part 1: Part one' }).getByRole('link', { name: 'Lesson two' }).click();
18+
19+
await expect(page.getByRole('heading', { level: 1, name: 'Lessons in part test - Lesson two' })).toBeVisible();
20+
await expect(page.getByText('Lesson in part without chapter')).toBeVisible();
21+
22+
await expect(async () => {
23+
const button = page.getByRole('button', { name: 'Part one / Lesson two' });
24+
await button.click();
25+
await expect(page.locator('[data-state="open"]', { has: button })).toBeVisible({ timeout: 50 });
26+
}).toPass();
27+
28+
// expand part
29+
await navigation.getByRole('button', { name: 'Part 2: Part two' }).click();
30+
31+
// expand chapter
32+
await navigation
33+
.getByRole('region', { name: 'Part 2: Part two' })
34+
.getByRole('button', { name: 'Chapter one' })
35+
.click();
36+
37+
// select lesson
38+
await navigation.getByRole('region', { name: 'Chapter one' }).getByRole('link', { name: 'Lesson three' }).click();
39+
40+
await expect(page.getByRole('heading', { level: 1, name: 'Lessons in part test - Lesson three' })).toBeVisible();
41+
await expect(page.getByText('Lesson in chapter')).toBeVisible();
42+
});
43+
44+
test('user can navigate between lessons using nav bar links', async ({ page }) => {
45+
await page.goto('/');
46+
await expect(page.getByRole('heading', { level: 1, name: 'Lessons in part test - Lesson one' })).toBeVisible();
47+
await expect(page.getByText('Lesson in part without chapter')).toBeVisible();
48+
49+
await navigateToPage('Lesson two');
50+
await expect(page.getByText('Lesson in part without chapter')).toBeVisible();
51+
52+
await navigateToPage('Lesson three');
53+
await expect(page.getByText('Lesson in chapter')).toBeVisible();
54+
55+
await navigateToPage('Lesson four');
56+
await expect(page.getByText('Lesson in chapter')).toBeVisible();
57+
58+
async function navigateToPage(title: string) {
59+
await page.getByRole('link', { name: title }).click();
60+
await expect(page.getByRole('heading', { level: 1, name: `Lessons in part test - ${title}` })).toBeVisible();
61+
}
62+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test('user can navigate between lessons using breadcrumbs', async ({ page }) => {
4+
await page.goto('/lesson-one');
5+
6+
await expect(page.getByRole('heading', { level: 1, name: 'Lessons in root test - Lesson one' })).toBeVisible();
7+
await expect(page.getByText('Lesson in root without part')).toBeVisible();
8+
9+
// navigation select can take a while to hydrate on page load, click until responsive
10+
await expect(async () => {
11+
const button = page.getByRole('button', { name: 'Lesson one' });
12+
await button.click();
13+
await expect(page.locator('[data-state="open"]', { has: button })).toBeVisible({ timeout: 50 });
14+
}).toPass();
15+
16+
await page.getByRole('navigation').getByRole('link', { name: 'Lesson two' }).click();
17+
18+
await expect(page.getByRole('heading', { level: 1, name: 'Lessons in root test - Lesson two' })).toBeVisible();
19+
});

packages/astro/src/default/pages/index.astro

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import { joinPaths } from '../utils/url';
44
55
const tutorial = await getTutorial();
66
7-
const part = tutorial.parts[tutorial.firstPartId!];
8-
const chapter = part.chapters[part?.firstChapterId!];
9-
const lesson = chapter.lessons[chapter?.firstLessonId!];
7+
const lesson = tutorial.lessons[0];
8+
const part = lesson.part && tutorial.parts[lesson.part.id];
9+
const chapter = lesson.chapter && part?.chapters[lesson.chapter.id];
1010
11-
const redirect = joinPaths(import.meta.env.BASE_URL, `/${part.slug}/${chapter.slug}/${lesson.slug}`);
11+
const slug = [part?.slug, chapter?.slug, lesson.slug].filter(Boolean).join('/');
12+
13+
const redirect = joinPaths(import.meta.env.BASE_URL, `/${slug}`);
1214
---
1315

1416
<!doctype html>

0 commit comments

Comments
 (0)