Loading Your Own JSON
The site's Projects page builds its cards from an array written inside js/projects.js. That works, but adding a project means editing code, and a slip in the code can break the whole page. Moving the projects into a data file of their own separates the two: the list becomes something anyone can edit, and the code only reads it. It is also your first real fetch, made from your own site.
Think of a restaurant that paints its menu straight onto the wallpaper. Every new dish means redecorating. Put the menu on a board instead and the kitchen changes a dish with a piece of chalk, while the room stays as it is. The data file is the board, and the script is the room that displays whatever the board says.
The Data File
Make a folder called data in my-site, and in it a file called projects.json. It holds the same three projects as before, with the same four fields, written as JSON by the rules of Chapter 7: double quotes around every key and every string, no comma after the last item, no comments.
[
{
"title": "Budget Tracker",
"description": "Adds up a month of spending from a spreadsheet.",
"tags": ["javascript"],
"url": "https://example.com/budget"
},
{
"title": "Recipe Cards",
"description": "A responsive page of recipes that prints cleanly.",
"tags": ["html", "css"],
"url": "https://example.com/recipes"
},
{
"title": "Weather Now",
"description": "Shows today's forecast for any city.",
"tags": ["javascript", "api"],
"url": "https://example.com/weather"
}
]
The file is one array of three objects: Budget Tracker, Recipe Cards and Weather Now, each with its title, description, tags and address. Nothing in it is code. It is text that describes data, which is what JSON is for. Once the next step works, delete the old projects array from js/projects.js, so the list lives in one place only.
Loading It: loadProjects
A new function in js/projects.js fetches the file and returns the array. It is the pattern from the previous topic with the site's names on it.
export async function loadProjects() { const response = await fetch("data/projects.json"); if (!response.ok) { throw new Error(`Could not load projects: ${response.status}`); } const projects = await response.json(); return projects; }
loadProjects is exported so main.js can import it. It asks for the file, throws an error if the answer is not OK, waits for the body as JSON, and returns the array. Because the function is async, whoever calls it gets a promise and can await it.
One detail about the address. A path given to fetch is relative to the page's address, not to the script file. The page is projects.html, at the top of the site, so data/projects.json means the data folder beside it, even though the code sits inside the js folder. Paths in an import line work the other way: they are relative to the file that imports.
Wiring It Up in main.js
The Projects page needs one new element: a place for messages about the loading. Put this paragraph in projects.html just above the element with the project-grid class. It is empty on purpose, and its role="status" makes screen readers announce whatever text the script puts in it, as the contact form's status did in Chapter 10.
<p id="projects-status" role="status"></p>
import { loadProjects, renderProjects } from "./projects.js"; let projects = []; const projectsStatus = document.querySelector("#projects-status"); async function main() { if (!projectsStatus) { return; } projectsStatus.textContent = "Loading projects..."; try { projects = await loadProjects(); renderProjects(projects); projectsStatus.textContent = ""; } catch (error) { projectsStatus.textContent = "Projects could not be loaded."; console.error(error); } } main();
These lines change the top and the end of main.js; the filter and the contact-form code stay where they are, in between. The import line no longer asks for projects, because projects.js no longer has an array to give. Instead main.js keeps its own projects, empty at first, and main fills it. The filter from Chapter 9 reads that same name, so it now filters the loaded list without any change.
Inside main, the guard returns early on pages without the status paragraph. Then the status says Loading projects, the function waits for the data, renders the cards with renderProjects from Chapter 8, and clears the status. If anything fails, the visitor gets one plain sentence, and console.error writes the full error, in red, in the Console for you. Open projects.html through the preview server and the three cards appear exactly as before. The difference is in the Network panel: a request for projects.json, status 200.
Why a Double-Clicked File Shows Nothing
Now open projects.html by double-clicking it in your file manager. The grid is empty and nothing explains why on the page. The Console has the reason: main.js is a module, and just as in Chapter 7, the browser refuses to load modules for a page opened straight from disk. Such a page has no proper web address, and browsers do not let its scripts read other files from your disk either. None of the code runs, not even the line that shows Loading projects.
Through the preview server, the page has a real address, and asking for data/projects.json is an ordinary request to the same server that sent the page. It is exactly the kind of request a page makes to any server on the internet. That is why this book has used the preview server from the start.
The Loading Message, and a Broken File
The preview server answers so fast that Loading projects flashes by unseen. To see it, open the Network panel's throttling menu, choose one of the slow presets, such as 3G, and reload: the message sits above the empty grid until the data arrives, then disappears as the cards appear. Set the menu back to No throttling afterwards. The message is written by the script, not typed into the HTML, for a reason: with JavaScript switched off, a Loading message in the HTML would stay there forever, promising something that never comes. The plain link list inside noscript from Chapter 8 covers that case instead.
Now break the data on purpose. Add a comma after the last project's closing brace, the most common JSON mistake there is, and save.
"url": "https://example.com/weather" }, ]
Reload the page. The file still arrives with status 200, but response.json() cannot parse it and its promise is rejected with a SyntaxError. loadProjects passes the error up, and the catch in main shows Projects could not be loaded. The Console has the details, including roughly where the parse failed. Without that catch, the visitor would see an empty grid and no words at all, which looks like a broken site. Remove the comma and the cards return.
- "Fetching a local file is different from fetching a server." Through the preview server it is the same kind of request, to the same kind of address.
- "If the file is missing, an empty grid is fine." An empty grid with no message looks like a bug. Say what happened.
- "JSON files can have comments." They cannot. A comment breaks the parse just as a trailing comma does.
- Content and code are now separate, the way real sites are built: a new project is a new entry in a data file.
- The loading and error states set the pattern for the live data at the end of this chapter.
Knowledge Check
Opened by double-clicking the file, projects.html shows no project cards. Why?
- The JSON file has to be opened in the browser first
- Pages opened from disk cannot load modules or fetch
- Double-clicked pages run their scripts after a delay
- The data file first needs to be moved into the js folder
The JSON file has a comma after its last project. What should the page do?
- Leave the grid empty, since nothing could be loaded
- Show the three cards and quietly skip the comma
- Show a message saying the projects could not load
- Reload itself until the file parses correctly
After this topic, where does the list of projects live?
- In an array exported from
js/projects.js - In the HTML of
projects.html, as three cards - In
localStorage, saved from the last visit - In
data/projects.json, fetched on load
Why is Loading projects written by the script instead of typed into the HTML?
- Without JavaScript, the message would never go away
- Screen readers skip any text written in the HTML
- HTML text cannot be changed once the page loads
- The browser hides HTML text until scripts run
You got correct