ENGLISH·COURSE API
    Preparing search index...
    • The course manifests the application serves: every declared course, minus the drafts when shouldShowDraftCourses says to hide them.

      Parameters

      • courses: readonly {
            id: string & $brand<"CourseId">;
            slug: string & $brand<"Slug">;
            title: string;
            description: string;
            language: string;
            sequence: number;
            draft: boolean;
            track: "level" | "reference";
            outcomes?: string[];
            audience?: string;
            highlights?: string[];
            sounds?: { vowels: string[]; consonants: string[] };
            translations?: Record<
                string,
                {
                    description?: string;
                    outcomes?: string[];
                    audience?: string;
                    highlights?: string[];
                },
            >;
            modules: {
                id: string & $brand<"ModuleId">;
                slug: string & $brand<"Slug">;
                title: string;
                sequence: number;
                lessons: (
                    | {
                        id: string & $brand<"LessonId">;
                        slug: string & $brand<"Slug">;
                        sequence: number;
                        title: string;
                        resources: {
                            id: string & $brand<(...)>;
                            title: string;
                            url: string;
                            kind: "pdf" | "slides" | "code" | "other";
                        }[];
                        kind: "video";
                        description: string;
                        source: string;
                        durationSeconds: number;
                        poster?: string;
                        notesKey?: string;
                        uploadDate?: string;
                    }
                    | {
                        id: string & $brand<"LessonId">;
                        slug: string & $brand<"Slug">;
                        sequence: number;
                        title: string;
                        resources: {
                            id: string & $brand<(...)>;
                            title: string;
                            url: string;
                            kind: "pdf" | "slides" | "code" | "other";
                        }[];
                        kind: "reading";
                        body: string;
                    }
                )[];
            }[];
        }[]

        The parsed manifests, in ladder order

      Returns readonly {
          id: string & $brand<"CourseId">;
          slug: string & $brand<"Slug">;
          title: string;
          description: string;
          language: string;
          sequence: number;
          draft: boolean;
          track: "level" | "reference";
          outcomes?: string[];
          audience?: string;
          highlights?: string[];
          sounds?: { vowels: string[]; consonants: string[] };
          translations?: Record<
              string,
              {
                  description?: string;
                  outcomes?: string[];
                  audience?: string;
                  highlights?: string[];
              },
          >;
          modules: {
              id: string & $brand<"ModuleId">;
              slug: string & $brand<"Slug">;
              title: string;
              sequence: number;
              lessons: (
                  | {
                      id: string & $brand<"LessonId">;
                      slug: string & $brand<"Slug">;
                      sequence: number;
                      title: string;
                      resources: {
                          id: string & $brand<(...)>;
                          title: string;
                          url: string;
                          kind: "pdf" | "slides" | "code" | "other";
                      }[];
                      kind: "video";
                      description: string;
                      source: string;
                      durationSeconds: number;
                      poster?: string;
                      notesKey?: string;
                      uploadDate?: string;
                  }
                  | {
                      id: string & $brand<"LessonId">;
                      slug: string & $brand<"Slug">;
                      sequence: number;
                      title: string;
                      resources: {
                          id: string & $brand<(...)>;
                          title: string;
                          url: string;
                          kind: "pdf" | "slides" | "code" | "other";
                      }[];
                      kind: "reading";
                      body: string;
                  }
              )[];
          }[];
      }[]

      The manifests to serve, order preserved

      Filtering the manifests before they are flattened is what makes every catalog surface agree without any of them knowing about drafts. A withheld course contributes no Course, so CourseRepository.bySlug returns null and the existing not-found state answers its URLs; it also contributes no module, lesson row, resource row or notes key, so nothing of it stays addressable by id.

      This is a visibility filter over the one source, not a second source: the decision reads each manifest's own draft field, so no configuration can introduce a course the manifests do not declare.

      Temporary by design. openspec/changes/archive/2026-09-08-hide-draft-courses-in-production/design.md carries the removal recipe: deleting "draft": true from a manifest publishes that course with no code change, and deleting this folder plus the one call in content-manifest.ts removes the flag entirely.

      InvalidDraftVisibilityError when SHOW_DRAFT_COURSES is set to an unrecognized value