How to Work on Workshops
Our workshops use a step-based approach to teach concepts to campers. A workshop will consist of multiple files, which we refer to as “steps”. These files are named by the challenge ID, to avoid issues with the translation flow. Unfortunately, this makes it difficult to find the file associated with a specific step.
We’ve built a challenge editor tool that helps remedy this. This tool allows you to navigate the available workshops, and the steps for each workshop (in order). There’s also an embedded code editor you can use to work on the files directly.
Styling pointers
Section titled “Styling pointers”Workshops should alternate steps in which new concepts are introduced piece by piece, and steps where there is freedom of implementation.
Instructions and hints
Section titled “Instructions and hints”Write instructions in the second person, using American English and short, direct sentences. State what campers need to change and where. Name the relevant file, function, variable, or element, and put code identifiers in backticks. Explain new concepts before asking campers to apply them.
Each hint should identify the requirement checked by its test and help campers understand what to fix. Keep the hint consistent with the instructions and assertion. For example:
- Instruction: Inside
greet, return the string"Hello". - Hint: Your
greetfunction should return the string"Hello".
Avoid vague hints such as “Your code is incorrect.”
Follow the challenge-description guidelines, challenge-text formatting rules, and Writing Style Guide for wording, code references, and punctuation.
Test assertions
Section titled “Test assertions”Choose tests based on what the step asks campers to do.
When the requirement is about behavior, run the code and assert the result. For example, call a function and check its return value, capture its console output, or check the state it changes. A test can also read a globally accessible variable to check its value.
When the requirement is about code structure, use the curriculum helpers to inspect that structure. For example, the JavaScript Explorer helpers can check whether a variable is declared inside a particular function. AST checks inspect the parsed code; they do not execute it or verify its behavior. A declaration can pass a structural check while the function still returns the wrong result.
Some steps need both kinds of checks. If a step asks campers to implement a function using a particular construct, test the function’s behavior and check the required construct separately. Only enforce syntax or implementation details that the instructions require, and allow alternative solutions that meet those requirements.
The Chai assertions are available in tests. Prefer specific assertion methods over the generic assert(). For example, use assert.strictEqual(a, 2) to check that a is the number 2. Avoid assert.equal(a, 2) for this check because it uses loose equality and also accepts the string "2".
Create the workshop
Section titled “Create the workshop”Workshops will have a dashedName of the kind workshop-cat-photo-app, that is the word workshop followed by the name of the app that is being built.
Workshop titles start with “Build a” or “Design a”, followed by the name of the app.
Debugging workshops have titles that start with “Debug a” and use the prefix workshop-debug- in the dashedName.
You can create the workshop with pnpm run create-new-project. The CLI will ask you which certification, chapter, module, and position the workshop belongs to. If you are unsure about these concepts, refer to the curriculum file structure page.
The metadata for workshops require a blockLabel property with a value of workshop, and a blockLayout with a value of challenge-grid.
Using the Challenge Editor
Section titled “Using the Challenge Editor”These instructions will tell you how to use our challenge editor tool to work on the workshops.
Setting Up the Challenge Editor
Section titled “Setting Up the Challenge Editor”The first time you want to use the editor, and after you use clean commands (like clean-and-develop), make sure you are in the root freeCodeCamp directory and run: pnpm challenge-editor-setup.
Starting the Editor
Section titled “Starting the Editor”Once the editor is set up, run pnpm run challenge-editor (again in the root directory) to start both the client and the API that powers the editor.
The client will run on port 3300, so you can access it at http://localhost:3300. The API runs on port 3200, to avoid conflicts with the learn client and server. This will allow you to run the freeCodeCamp application at the same time as the editor, so you can test your changes locally.
Navigating the Editor
Section titled “Navigating the Editor”The default view will list the available superblocks - these are the certifications. Click on the certification link you want to work on.
This will take you to the list of chapters. Click on one of these to be shown a list of modules. Then click on one of these to be shown a list of blocks. The blocks are the workshops. Click on the workshop link you want to work on.
This will take you to a list of steps for the workshop. If you are working on an existing step, you can click on the step link to open the editor. If you are adding or removing steps, click the Use the step tools link to switch to the step tools for that challenge.
Editing Steps
Section titled “Editing Steps”When you click on a step, you’ll be taken to the editor. This is a basic text editor that offers syntax highlighting.
After you have made your changes, click the Save Changes button to save your changes. You will get a browser alert letting you know that your changes are ready to commit.
Step Tools
Section titled “Step Tools”When you click the Use the step tools link, you’ll be taken to the step tools page. This allows you to add or remove steps from the workshop.
Create Next Step
Section titled “Create Next Step”Clicking this button will add a new step at the end of the workshop. This step will use the previous step’s code as the seed.
Create Empty Steps
Section titled “Create Empty Steps”Enter the number of steps you want to add in the input. Then, clicking the button will create many empty steps at the end of the workshop.
Insert Step
Section titled “Insert Step”Enter the step number that you want to add. Then, click the Insert Step button to add the step. The following steps will be re-ordered.
Delete Step
Section titled “Delete Step”Enter the step number you want to delete. Then click the Delete Step button to remove that step. This will automatically update the step numbers for the remaining steps.
Update Step Titles
Section titled “Update Step Titles”You should not have to use this tool unless you’ve manually deleted or added steps. This tool will reorder the step numbers.
Using the Scripts Manually
Section titled “Using the Scripts Manually”If you want to work on the steps manually, in your local IDE, you can run the step management scripts directly.
The tools/challenge-helper-scripts folder contains tools to help facilitate the creation and maintenance of the freeCodeCamp project-based curriculum.
Create a New Workshop
Section titled “Create a New Workshop”Run pnpm run create-new-project as described in Create the workshop. This opens up a command line UI that guides you through the process. Once that has finished, there should be a new challenge in the English curriculum that you can use for the first step of the workshop. For example, if you created a workshop called workshop-abc in the Responsive Web Design certification, it would be in curriculum/challenges/english/blocks/workshop-abc.
If you want to create new steps, the following tools simplify that process.
create-next-step
Section titled “create-next-step”A one-off script that will automatically add the next step based on the last step in the workshop. The challenge seed code will use the previous step’s challenge seed code, including its editable-region markers. Update the new seed to include the changes campers were asked to make in the previous step, and move the markers to the region needed for the new step.
How to Run the Script
Section titled “How to Run the Script”- Change to the directory of the workshop.
- Run the following command:
pnpm run create-next-stepcreate-empty-steps
Section titled “create-empty-steps”A one-off script that automatically adds a specified number of steps and updates the block’s challengeOrder. The generated files have no seed code and use challengeType: placeholder. Set the correct challenge type and add instructions, tests, seed code, and editable-region markers before using them.
How to Run the Script
Section titled “How to Run the Script”- Change to the directory of the workshop.
- Run the following command:
pnpm run create-empty-steps X # where X is the number of steps to create.insert-step
Section titled “insert-step”A one-off script that automatically adds a new step at a specified position, incrementing all subsequent steps (both their titles and in the block’s JSON structure file). The challenge seed code will use the previous step’s challenge seed code including its editable-region markers. Review the seed and reposition the markers for the inserted step. When inserting the first step, there is no previous seed to copy.
How to Run the Script
Section titled “How to Run the Script”- Change to the directory of the workshop.
- Run the following command:
pnpm run insert-step X # where X is the position to insert the new step.delete-step
Section titled “delete-step”A one-off script that deletes an existing step, decrementing all subsequent steps (both their titles and in the block’s JSON structure file)
How to Run the Script
Section titled “How to Run the Script”- Change to the directory of the workshop.
- Run the following command:
pnpm run delete-step X # where X is the step number to be deleted.update-step-titles
Section titled “update-step-titles”A one-off script that automatically updates the frontmatter in a workshop’s markdown files so that they are consistent with the block’s JSON structure file. It ensures that each step’s title (and dashedName) matches the block structure’s challengeOrder.
How to Run the Script
Section titled “How to Run the Script”- Change to the directory of the workshop.
- Run the following command:
pnpm run update-step-titlesStep File Template
Section titled “Step File Template”Each step in a workshop is a Markdown file named after its challenge id. The challengeType and seed language depend on the type of workshop:
challengeType | Workshop type | Seed language(s) |
|---|---|---|
0 | HTML/CSS | html only, or html + css, or html + css + js/jsx/ts/tsx |
1 | JavaScript/TypeScript | js or ts |
20 | Python | py |
Each step uses exactly two --fcc-editable-region-- markers across all its seed files to highlight where campers should make changes.
The last step requires a # --solutions-- section. Earlier steps normally use the next step’s seed as their solution, but can include an explicit solution when needed. See Seed continuity and solutions.
The demoType: onLoad field is only present in the first step, and only for workshops that have a visual output (i.e. HTML-based). It is not mandatory, if the workshop design does not require it, it can be omitted.
Here is the template for a JavaScript workshop step:
---id: <ObjectId>title: Step NchallengeType: 1dashedName: step-n---
# --description--
Step description in markdown.
# --hints--
Description of what the test checks.
```js// test code```
# --seed--
## --seed-contents--
```js// seed code--fcc-editable-region--
--fcc-editable-region--```
# --solutions--
```js// solution code (required for the last step, optional for earlier steps)```Formatting seed code
Section titled “Formatting seed code”Follow the seed-code formatting guidelines:
- Use two spaces per indentation level for HTML, CSS, and JavaScript, and four for Python.
- End JavaScript statements with semicolons.
- Use double quotes where applicable in HTML, CSS, and JavaScript, and single quotes in Python.
Keep formatting consistent between consecutive steps. Preserve indentation, quote style, and blank lines in code that the camper has not been asked to change. Do not introduce unrelated reformatting when preparing the next step’s seed.
Use seed comments sparingly and follow the seed-code comment rules. Comments must match an entry in the comment dictionary, including its case and spacing.
Editable regions and indentation
Section titled “Editable regions and indentation”Place the editable region around the code campers need to change in that step. When campers need to insert code on a blank line, pre-indent that line to match the surrounding code. For example, code inside this JavaScript function needs two leading spaces:
function greet() {--fcc-editable-region--
--fcc-editable-region--}The blank line between the markers contains two spaces. Keep those spaces when saving the file; indenting the marker lines does not indent the camper’s code. Use the indentation required by the language and nesting level, such as four spaces for a Python function body.
Review the editable-region placement and indentation after adding or inserting steps. Copied markers can still point to the previous step’s editing area.
Seed continuity and solutions
Section titled “Seed continuity and solutions”Each step’s seed should include the completed changes from the previous step. The step-generation tools copy seed code; they do not solve the previous step for you. For steps without an explicit # --solutions-- section, curriculum tests use the next step’s seed as the solution to the current step.
For example, if Step 2 asks campers to add return "Hello"; inside a function, Step 3’s seed should already contain that statement. The final step needs an explicit solution because there is no next step to supply one.
An earlier step can also need an explicit solution when it intentionally introduces a TypeScript error. For example, Step 4 of the Type-Safe Math Toolkit workshop asks campers to add a number annotation to a function parameter while its call still passes a string. The step’s seed and explicit solution include "noCheck": true so its AST test can run without type checking rejecting the code.
The next step keeps the completed TypeScript code but omits that compiler setting so campers can see and fix the type error. Using that next step’s seed as the previous step’s solution would therefore fail type checking. The explicit solution preserves the compiler setting needed to test the previous step:
# --solutions--
```tsfunction square(num: number) { return num * num;}
const result = square("something");
console.log(result);```
```json{ "compilerOptions": { "noCheck": true }}```Do not include editable-region markers in solutions. An explicit solution supplies the code and configuration used to validate that step’s tests.
Hooks are optional code blocks that run at specific points during test execution. They can be used to set up shared state, install fake timers, or clean up after tests.
Hooks are supported for workshop types 0, 1, and 20.
Four hooks are available:
| Hook | When it runs |
|---|---|
--before-all-- | Once, before any test runs |
--before-each-- | Before each individual test |
--after-each-- | After each individual test (in a finally block, so it always runs even if the test fails) |
--after-all-- | Once, after all tests have finished |
Execution order:
For challenges that have an HTML file, --before-all-- is injected as a <script> tag into the sandboxed iframe before the user’s HTML is parsed, so it runs before user code. It has full access to DOM APIs and global test helpers like __FakeTimers and $ (jQuery). The user’s code then renders into the page, and each test runs against the live DOM:
1. --before-all--
2. user code (evaluated once)
3. (for each test): --before-each-- → test → --after-each--
4. --after-all--For JavaScript and TypeScript challenges without an HTML file, --before-all-- runs once before any test. Each test evaluates --before-each--, the compiled user code, and the test together:
1. --before-all--
2. (for each test): --before-each-- → user code → test → --after-each--
3. --after-all--For Python challenges, hooks are JavaScript code. Each test gets fresh Python globals. The evaluator first evaluates --before-each-- and the JavaScript test wrapper. If the wrapper returns an object with a test function, the evaluator then runs the user’s Python code with runPython and calls that function:
1. --before-all--
2. (for each test): --before-each-- → evaluate test wrapper → user code → test() → --after-each--
3. --after-all--Plain JavaScript assertions without a test function run while the wrapper is evaluated and do not automatically execute the user’s Python code.
For both kinds of challenges without HTML, variables declared with let or const in --before-all-- are not available in tests or --after-all--, because these run in separate evaluations. To share JavaScript state, assign it to globalThis and remove it during cleanup. For example:
# --before-all--
```jsglobalThis.testState = { count: 0 };```
# --after-all--
```jsdelete globalThis.testState;```Syntax:
Hooks use the same # --hook-name-- heading syntax as other sections. Each hook must contain exactly one code block. Hooks are placed after --description-- and before --hints--:
The following fake-timer example is for HTML-based challenges only. __FakeTimers is not available in the JavaScript or Python workers.
# --description--
...
# --before-all--
```js// Runs once before any test. Set up shared state here.let clock = __FakeTimers.install();```
# --before-each--
```js// Runs before each test.```
# --after-each--
```js// Runs after each test, even if it fails.```
# --after-all--
```js// Runs once after all tests. Clean up here.clock.uninstall();```
# --hints--
...Proposing a Pull Request (PR)
Section titled “Proposing a Pull Request (PR)”After you’ve committed your changes, check here for how to open a Pull Request.