NASA Astronomy Photo of the Day

Build a space-themed website that fetches NASA's daily astronomy photo via API.

Introduction

30 Second Summary

Every day, NASA publishes a brand new astronomy photo selected by professional astronomers. These images range from distant galaxies to close-ups of planets, and they're all available for free through a public API.

In this project, you will build a space-themed website that connects to NASA's real Astronomy Picture of the Day API. Your site will display stunning space imagery with scientific explanations, and let you browse photos from any date going back to 1995.

What You'll Build

A single-page website with a dark space theme that fetches and displays NASA's Astronomy Picture of the Day using HTML, CSS, and JavaScript.

By the end of this project, you'll have:

  • A live website that displays today's NASA astronomy photo with its title, image, and scientific explanation.
  • A working date picker that lets you browse any astronomy photo since June 16, 1995.
  • A deployed site on Vercel with a shareable public URL anyone can visit.
  • Secret Mission: A "Random Photo" button that fetches a surprise astronomy image from a random date.

Are there any prerequisites?

None. This guide includes setup for all required tools. You just need a computer with a web browser.

Before We Start

You are about to build a website that connects to NASA's real Astronomy Picture of the Day API. Every day, NASA publishes a stunning space image along with a description written by a professional astronomer.

Before you start coding, you need a project folder and three empty files that will hold your website's structure, styling, and logic. Let's get those set up now.

In this step, get ready to:
  • Create a project folder on your desktop.
  • Open it in VS Code.
  • Create three files for your website.
Create your project folder

Your project needs its own folder to keep all your files together. You will create this folder on your Desktop so it is easy to find.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Open the integrated terminal by pressing Ctrl+` (Windows) or Cmd+` (macOS).

What is the integrated terminal?

The integrated terminal is a command line built right into VS Code. It lets you run commands without switching to a separate application.

  • Move to your Desktop by running this command:

macOS

cd ~/Desktop

Windows

cd $HOME\Desktop
  • Create your project folder and move into it by running these commands:
mkdir nasa-apod
cd nasa-apod

What does this do?

The mkdir command creates a new folder called nasa-apod. The cd command moves you inside that folder so your next commands run from there.

Create your project files

Every website needs at least three files: one for structure, one for styling, and one for behavior.

  • Create three empty files by running this command:

macOS

touch index.html style.css script.js

Windows

New-Item index.html, style.css, script.js
  • Confirm the files exist by running:
ls

You should see index.html, style.css, and script.js listed in the output.

Don't see all three files?

Make sure you ran the file creation command from inside the nasa-apod folder. If ls shows other files like Documents or Downloads, you are still in the wrong directory. Run cd ~/Desktop/nasa-apod (macOS) or cd $HOME\Desktop\nasa-apod (Windows) and try again.

Still stuck? Help me figure out why my files aren't appearing in the nasa-apod folder.

Open the project folder in VS Code

Now that your files exist, open the folder as a workspace in VS Code. This lets the Explorer sidebar show your project structure.

  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the nasa-apod folder, and click Open.

Your Explorer sidebar on the left should now show index.html, style.css, and script.js under the NASA-APOD heading.

Don't see the Explorer sidebar?

Click the top icon in the left sidebar (it looks like two overlapping documents) or press Cmd+Shift+E (macOS) or Ctrl+Shift+E (Windows) to toggle it open.

Need more help? Help me find my project files in VS Code's Explorer sidebar.

Your workspace is ready. Next up, you will build out the HTML structure and connect it to the NASA API.

Set Up the HTML Structure

Your project folder is ready with three empty files waiting to be filled. The first one to tackle is index.html, which defines the skeleton of your webpage. Every element the user sees or interacts with starts here.

In this step, you will write the HTML structure that holds the page title, a date picker, loading and error messages, and the photo content area. Once it is complete, you will preview it in the browser to confirm everything is in place.

In this step, get ready to:
  • Add the HTML5 boilerplate with a linked stylesheet and script file.
  • Build the page elements for the date picker, loading state, error message, and photo content area.
  • Open the page in a browser to confirm it loads without errors.
Add the HTML5 boilerplate and page structure

Your HTML file needs three things: a <head> section that connects your stylesheet and sets the page title, a <body> with placeholder elements for all the dynamic content, and a script tag that loads your JavaScript.

  • In VS Code, click on index.html in the Explorer sidebar to open it.
  • Copy the following HTML into the file:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>NASA Astronomy Photo of the Day</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1>Astronomy Photo of the Day</h1>
        <div class="controls">
            <input type="date" id="date-picker">
        </div>
        <div id="loading" class="loading">Loading...</div>
        <div id="error" class="error"></div>
        <div id="content" class="content">
            <h2 id="photo-title"></h2>
            <div id="media-container" class="media-container"></div>
            <p id="date-display" class="date-display"></p>
            <p id="explanation" class="explanation"></p>
            <p id="copyright" class="copyright"></p>
        </div>
    </div>
    <script src="script.js" defer></script>
</body>
</html>

What does this code do?

The <head> section connects your stylesheet (style.css) and sets the page title that appears in the browser tab.

Inside the body, you have a .container div that wraps everything. The #date-picker input lets users pick a date. The #loading and #error elements are hidden by default and only appear when your JavaScript needs them.

The #content section holds all the photo-related elements: a title, the image/video container, the date display, an explanation paragraph, and copyright text. Your JavaScript will fill these in later.

The defer attribute on the script tag tells the browser to load script.js after the HTML has finished parsing. This ensures all elements exist before your code tries to access them.

  • Save index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).

Don't see anything in the file?

Make sure you clicked on index.html in the Explorer sidebar (not style.css or script.js). The tab at the top of the editor should show the filename.

If you accidentally pasted into the wrong file, press Cmd+Z (macOS) or Ctrl+Z (Windows) to undo, then open the correct file.

Help me figure out why my HTML file is empty.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete index.html file. Compare it against yours:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>NASA Astronomy Photo of the Day</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1>Astronomy Photo of the Day</h1>
        <div class="controls">
            <input type="date" id="date-picker">
        </div>
        <div id="loading" class="loading">Loading...</div>
        <div id="error" class="error"></div>
        <div id="content" class="content">
            <h2 id="photo-title"></h2>
            <div id="media-container" class="media-container"></div>
            <p id="date-display" class="date-display"></p>
            <p id="explanation" class="explanation"></p>
            <p id="copyright" class="copyright"></p>
        </div>
    </div>
    <script src="script.js" defer></script>
</body>
</html>
Preview the page in your browser

Before adding styles or JavaScript, confirm the raw HTML loads correctly. This catches typos and structural errors early.

  • Open File Explorer (Windows) or Finder (macOS) and navigate to the nasa-apod folder on your Desktop.
  • Double-click index.html to open it in your default browser.
  • Open the browser console by pressing F12 (or Cmd+Option+J on macOS in Chrome).
  • Check that the Console tab shows no red error messages.

You should see unstyled text on a white page. The heading "Astronomy Photo of the Day" appears at the top, followed by a date picker input, the word "Loading...", and some empty space where the content elements are waiting.

Seeing a blank page?

If the page is completely blank, check that you saved index.html in VS Code. A white dot on the file tab means unsaved changes.

If you see an error in the console about style.css or script.js not being found, that is expected. Those files are empty right now and the browser is just noting they have no content yet.

Help me troubleshoot why my HTML page is not displaying correctly.

Your HTML skeleton is complete. Every element has an ID that your JavaScript will use to find and update it with NASA's data. Next up, you will style this page with a dark space theme to match the cosmic content it will display.

Style the Space Theme

Your HTML structure is in place, but right now the page looks like plain text on a white background. A website about space photos deserves a dark, cinematic feel that lets the imagery shine.

In this step, you will transform the page into a dark space-themed layout using CSS. You will style the background, typography, date input, and content areas so everything feels polished before any data loads.

In this step, get ready to:
  • Set up a dark background with centered layout and clean typography.
  • Style the date picker input and media display area.
  • Add hidden state classes and a responsive mobile layout.
Set up the dark background and page layout

The foundation of the space theme is a deep navy background with light text. You will also reset default browser spacing and center all the content in a narrow column.

  • Switch back to VS Code. Open style.css and add the base layout styles by pasting this code:
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    background-color: #0b0d17;
    color: #e0e0e0;
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    line-height: 1.6;
    min-height: 100vh;
}

.container {
    max-width: 900px;
    margin: 0 auto;
    padding: 2rem 1.5rem;
}

h1 {
    text-align: center;
    font-size: 2rem;
    margin-bottom: 1.5rem;
    color: #ffffff;
}

.controls {
    display: flex;
    justify-content: center;
    margin-bottom: 2rem;
}

What does this code do?

  • The * selector resets all default margins and padding so every browser starts from the same baseline.
  • The body gets a deep navy background (#0b0d17) with light gray text, creating the dark space feel.
  • The .container limits content to 900px wide and centers it horizontally with margin: 0 auto.
  • The .controls div uses flexbox to center the date picker horizontally.
  • Save style.css.
  • Switch to your browser and refresh the page. You should see a dark navy background with white centered text for the heading "Astronomy Photo of the Day" and the date input below it.

Still seeing a white background?

  • Make sure you saved style.css. Unsaved changes do not appear in the browser.
  • Confirm the <link rel="stylesheet" href="style.css"> tag exists in the <head> of your index.html. The filename must match exactly (case-sensitive).
  • Try a hard refresh with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows) to bypass the browser cache.

Why isn't my CSS loading in the browser?

Style the date input and media display

The default browser date input looks out of place on a dark background. You will restyle it to match the space theme and prepare the media area for displaying responsive images and videos.

  • In style.css, add the date picker styles below the .controls block by pasting this code:
#date-picker {
    background-color: #1a1d2e;
    color: #ffffff;
    border: 1px solid #3a3f5c;
    padding: 0.6rem 1rem;
    border-radius: 8px;
    font-size: 1rem;
    cursor: pointer;
}

#date-picker:focus {
    outline: none;
    border-color: #6c7ae0;
    box-shadow: 0 0 8px rgba(108, 122, 224, 0.3);
}

What does this code do?

  • The #date-picker selector targets the date input by its ID and gives it a dark background (#1a1d2e) with white text and rounded corners.
  • The :focus pseudo-class adds a purple glow effect when the learner clicks on the input. This provides a clear visual signal that the input is active.
  • Save style.css and refresh your browser.
  • You should see the date input has a dark background with rounded corners. Click on it to see the purple border glow appear.

Date input still looks like default browser styling?

  • Check that your #date-picker selector uses a hash (#) not a dot (.). IDs use # while classes use . in CSS.
  • Verify that the id="date-picker" in your HTML matches the CSS selector exactly.

Why isn't my date picker styling showing up?

  • Now add the media container styles below the #date-picker:focus block by pasting this code:
.media-container {
    text-align: center;
    margin-bottom: 1.5rem;
}

.media-container img {
    max-width: 100%;
    height: auto;
    border-radius: 12px;
}

.media-container iframe {
    width: 100%;
    max-width: 800px;
    height: 450px;
    border: none;
    border-radius: 12px;
}

What does this code do?

  • The .media-container img rule makes images responsive. max-width: 100% means images scale down to fit smaller screens but never stretch beyond their natural size.
  • The .media-container iframe rule handles video entries from NASA's archive. Some days feature YouTube embeds instead of images.
  • Both get border-radius: 12px for soft rounded corners that match the overall design.
  • Save style.css.

You will not see a visible change yet because no image is loaded. These styles activate once JavaScript fetches a photo from NASA in the next step.

Add loading states and responsive styles

Your page needs a loading indicator, an error message style, and content typography. These elements start hidden and only appear when JavaScript controls their visibility.

  • In style.css, add the state and content styles below the .media-container iframe block by pasting this code:
.loading {
    text-align: center;
    font-size: 1.2rem;
    color: #8888aa;
    display: none;
}

.error {
    text-align: center;
    color: #ff6b6b;
    font-size: 1rem;
    margin-bottom: 1rem;
    display: none;
}

.content {
    display: none;
}

h2 {
    font-size: 1.5rem;
    margin-bottom: 1rem;
    color: #ffffff;
    text-align: center;
}

.date-display {
    text-align: center;
    color: #8888aa;
    font-size: 0.9rem;
    margin-bottom: 1rem;
}

.explanation {
    font-size: 1rem;
    color: #cccccc;
    text-align: left;
    margin-bottom: 1rem;
}

.copyright {
    font-size: 0.85rem;
    color: #666688;
    text-align: center;
    font-style: italic;
}

What does this code do?

  • The .loading, .error, and .content classes all have display: none so they are hidden by default. JavaScript will toggle them visible at the right time.
  • The .error class uses #ff6b6b (a soft red) to make error messages stand out against the dark background.
  • The .explanation and .copyright styles use progressively more muted colors to create a visual hierarchy from the bright title down to the subtle credits.
  • Save style.css.

You will not see a visible change yet because these elements are hidden until JavaScript shows them. The next chunk adds the final piece that you can verify visually.

  • Add the responsive media query at the bottom of style.css by pasting this code:
@media (max-width: 600px) {
    .container {
        padding: 1rem;
    }

    h1 {
        font-size: 1.5rem;
    }

    .media-container iframe {
        height: 250px;
    }
}

What does this code do?

The @media query activates when the browser window is 600px wide or narrower (typical phone screens). It reduces padding and font sizes so the content fits comfortably on small screens without horizontal scrolling.

  • Save style.css and refresh your browser.
  • Resize your browser window to be very narrow (or press F12 to open DevTools and toggle the device toolbar). You should see the heading shrink and the padding tighten when the window is phone-sized.

No change when you resize?

  • Make sure the @media block is at the very bottom of your CSS file, after all other styles.
  • Check that you have max-width: 600px in the media query. If the value is wrong, the breakpoint will not trigger at the expected size.
  • Verify your browser window is actually narrow enough. Drag the edge until it is clearly phone-sized.

Why isn't my media query working?

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete style.css file. Compare it against yours:

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    background-color: #0b0d17;
    color: #e0e0e0;
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    line-height: 1.6;
    min-height: 100vh;
}

.container {
    max-width: 900px;
    margin: 0 auto;
    padding: 2rem 1.5rem;
}

h1 {
    text-align: center;
    font-size: 2rem;
    margin-bottom: 1.5rem;
    color: #ffffff;
}

.controls {
    display: flex;
    justify-content: center;
    margin-bottom: 2rem;
}

#date-picker {
    background-color: #1a1d2e;
    color: #ffffff;
    border: 1px solid #3a3f5c;
    padding: 0.6rem 1rem;
    border-radius: 8px;
    font-size: 1rem;
    cursor: pointer;
}

#date-picker:focus {
    outline: none;
    border-color: #6c7ae0;
    box-shadow: 0 0 8px rgba(108, 122, 224, 0.3);
}

.loading {
    text-align: center;
    font-size: 1.2rem;
    color: #8888aa;
    display: none;
}

.error {
    text-align: center;
    color: #ff6b6b;
    font-size: 1rem;
    margin-bottom: 1rem;
    display: none;
}

.content {
    display: none;
}

h2 {
    font-size: 1.5rem;
    margin-bottom: 1rem;
    color: #ffffff;
    text-align: center;
}

.media-container {
    text-align: center;
    margin-bottom: 1.5rem;
}

.media-container img {
    max-width: 100%;
    height: auto;
    border-radius: 12px;
}

.media-container iframe {
    width: 100%;
    max-width: 800px;
    height: 450px;
    border: none;
    border-radius: 12px;
}

.date-display {
    text-align: center;
    color: #8888aa;
    font-size: 0.9rem;
    margin-bottom: 1rem;
}

.explanation {
    font-size: 1rem;
    color: #cccccc;
    text-align: left;
    margin-bottom: 1rem;
}

.copyright {
    font-size: 0.85rem;
    color: #666688;
    text-align: center;
    font-style: italic;
}

@media (max-width: 600px) {
    .container {
        padding: 1rem;
    }

    h1 {
        font-size: 1.5rem;
    }

    .media-container iframe {
        height: 250px;
    }
}

Your page now looks like a proper space observatory interface. Next up, you will connect to NASA's real API and fetch today's astronomy photo to fill in that empty content area.

Fetch Today's Photo from NASA

Your page has its dark space theme, but it is still just static HTML. Time to bring it to life with real data from NASA.

NASA provides a free public API called APOD (Astronomy Picture of the Day) that returns a different stunning space image every single day. In this step, you will write JavaScript that calls this API, grabs today's photo, and displays it on your page.

In this step, get ready to:
  • Grab references to your HTML elements and set up API constants.
  • Create helper functions to manage the date picker.
  • Write an async function that fetches today's photo from NASA and displays it on the page.
Set up DOM references and helper functions

Before your JavaScript can update anything on the page, it needs to know which HTML elements to target. You will store references to each element at the top of your file so you can reuse them throughout the script.

  • Switch back to VS Code and open script.js.
  • Add the following code to grab references to your HTML elements and define your API constants:
const datePicker = document.getElementById('date-picker');
const loadingEl = document.getElementById('loading');
const errorEl = document.getElementById('error');
const contentEl = document.getElementById('content');
const photoTitle = document.getElementById('photo-title');
const mediaContainer = document.getElementById('media-container');
const dateDisplay = document.getElementById('date-display');
const explanationEl = document.getElementById('explanation');
const copyrightEl = document.getElementById('copyright');

// NASA APOD API endpoint and demo key
const API_URL = 'https://api.nasa.gov/planetary/apod';
const API_KEY = 'DEMO_KEY';

What does this code do?

Each document.getElementById() call finds an HTML element by its id attribute and stores it in a variable. This means you can reference loadingEl later instead of searching the page every time.

The two constants define where to send your API request. DEMO_KEY is a free key NASA provides for testing. It allows approximately 50 requests per day per IP address.

Now you need helper functions to manage the date picker. The APOD archive goes back to June 16, 1995, and you do not want users to pick a future date.

  • Add these two helper functions below your constants in script.js:
// Returns today's date as a YYYY-MM-DD string
function getTodayString() {
    const today = new Date();
    return today.toISOString().split('T')[0];
}

// Configures the date picker with valid boundaries
function setupDatePicker() {
    datePicker.max = getTodayString();
    datePicker.min = '1995-06-16';
    datePicker.value = getTodayString();
}

What does this code do?

getTodayString() creates a Date object for right now and converts it to the YYYY-MM-DD format that the NASA API expects. The .split('T')[0] part chops off the time portion from the ISO string.

setupDatePicker() sets three properties on the date input: max prevents picking a future date, min prevents going earlier than the first APOD photo, and value pre-fills today's date.

  • Save script.js.
  • You will not see a visible change yet. The next chunk uses these helpers to load the actual photo.
Fetch today's photo from the NASA API

This is the core of your site. You will write an async function that calls the NASA APOD API, handles the response, and updates the page with the photo data.

  • Add the following fetchAPOD function and initialization calls below your helper functions in script.js:
// Fetches a photo from the NASA APOD API and updates the page
async function fetchAPOD(date) {
    // Show loading state and hide previous content
    loadingEl.style.display = 'block';
    errorEl.style.display = 'none';
    contentEl.style.display = 'none';
    datePicker.disabled = true;

    try {
        // Build the API URL with the key and optional date
        let url = `${API_URL}?api_key=${API_KEY}`;
        if (date) {
            url += `&date=${date}`;
        }

        const response = await fetch(url);

        if (!response.ok) {
            throw new Error(`API error: ${response.status}`);
        }

        const data = await response.json();

        // Update the page with the returned data
        photoTitle.textContent = data.title;
        dateDisplay.textContent = data.date;
        explanationEl.textContent = data.explanation;

        // Show copyright if available
        if (data.copyright) {
            copyrightEl.textContent = `Image Credit: ${data.copyright}`;
            copyrightEl.style.display = 'block';
        } else {
            copyrightEl.style.display = 'none';
        }

        // Handle video vs image media types
        if (data.media_type === 'video') {
            mediaContainer.innerHTML = `<iframe src="${data.url}" allowfullscreen></iframe>`;
        } else {
            mediaContainer.innerHTML = `<img src="${data.url}" alt="${data.title}">`;
        }

        // Hide loading and show content
        loadingEl.style.display = 'none';
        contentEl.style.display = 'block';
    } catch (error) {
        // Show error message if fetch fails
        loadingEl.style.display = 'none';
        errorEl.textContent = 'Failed to load the astronomy photo. Please try again.';
        errorEl.style.display = 'block';
    } finally {
        // Always re-enable the date picker
        datePicker.disabled = false;
    }
}

// Set up the date picker and load today's photo
setupDatePicker();
fetchAPOD();

What does this code do?

The function starts by showing a loading message and disabling the date picker so the user cannot fire duplicate requests. Inside the try block, it builds the API URL, fetches the data, and checks if the response was successful.

Once JSON is parsed, it updates every element on the page: the title, date, explanation, and copyright. The media_type check handles the fact that some APOD entries are YouTube or Vimeo videos instead of images. Videos get rendered as an iframe, while images get a standard img tag.

The catch block shows a friendly error message if anything goes wrong. The finally block always re-enables the date picker, even after an error.

The last two lines at the bottom actually run the setup. setupDatePicker() configures the date boundaries, and fetchAPOD() (called without a date argument) fetches today's photo automatically.

What is async/await?

The Fetch API sends a network request that takes time to complete. The async keyword lets you use await inside a function, which pauses execution until the network response arrives.

Without await, your code would try to use the response before it exists. With it, JavaScript waits for the data, then continues to the next line.

  • Save script.js.
  • Refresh your browser. You should see today's NASA Astronomy Picture of the Day with its title, the image (or a video embed), a date, an explanation paragraph, and possibly a copyright line.

Not seeing a photo?

  • Open the browser console with F12 and check for errors. A typo in an element ID (like date-picker vs datePicker) is the most common cause.
  • If you see a 429 status error, you have hit the DEMO_KEY rate limit (approximately 50 requests per day). Wait a few minutes and try again.
  • Make sure script.js is saved and your browser is loading the correct local file (check the URL bar shows a file path to your nasa-apod folder).

Help me debug why my NASA APOD fetch is not displaying a photo.

Double check your code

✔️ Awesome, I've got everything!

Great. Make sure you have saved script.js and your browser is showing today's astronomy photo.

ⓧ I'd like to double check the full code

Here is the complete script.js file up to this point:

const datePicker = document.getElementById('date-picker');
const loadingEl = document.getElementById('loading');
const errorEl = document.getElementById('error');
const contentEl = document.getElementById('content');
const photoTitle = document.getElementById('photo-title');
const mediaContainer = document.getElementById('media-container');
const dateDisplay = document.getElementById('date-display');
const explanationEl = document.getElementById('explanation');
const copyrightEl = document.getElementById('copyright');

const API_URL = 'https://api.nasa.gov/planetary/apod';
const API_KEY = 'DEMO_KEY';

function getTodayString() {
    const today = new Date();
    return today.toISOString().split('T')[0];
}

function setupDatePicker() {
    datePicker.max = getTodayString();
    datePicker.min = '1995-06-16';
    datePicker.value = getTodayString();
}

async function fetchAPOD(date) {
    loadingEl.style.display = 'block';
    errorEl.style.display = 'none';
    contentEl.style.display = 'none';
    datePicker.disabled = true;

    try {
        let url = `${API_URL}?api_key=${API_KEY}`;
        if (date) {
            url += `&date=${date}`;
        }

        const response = await fetch(url);

        if (!response.ok) {
            throw new Error(`API error: ${response.status}`);
        }

        const data = await response.json();

        photoTitle.textContent = data.title;
        dateDisplay.textContent = data.date;
        explanationEl.textContent = data.explanation;

        if (data.copyright) {
            copyrightEl.textContent = `Image Credit: ${data.copyright}`;
            copyrightEl.style.display = 'block';
        } else {
            copyrightEl.style.display = 'none';
        }

        if (data.media_type === 'video') {
            mediaContainer.innerHTML = `<iframe src="${data.url}" allowfullscreen></iframe>`;
        } else {
            mediaContainer.innerHTML = `<img src="${data.url}" alt="${data.title}">`;
        }

        loadingEl.style.display = 'none';
        contentEl.style.display = 'block';
    } catch (error) {
        loadingEl.style.display = 'none';
        errorEl.textContent = 'Failed to load the astronomy photo. Please try again.';
        errorEl.style.display = 'block';
    } finally {
        datePicker.disabled = false;
    }
}

setupDatePicker();
fetchAPOD();

Your page now pulls real data from space. Next up, you will add date navigation so users can browse any astronomy photo from the past 30 years.

Add Date Navigation and Error Handling

Your website already loads today's astronomy photo from NASA. That proves the API connection works and your fetch logic is solid.

Now you will make the date picker interactive so users can browse any photo from NASA's archive going back to 1995. You will also test the error handling that is already built into your fetchAPOD function to confirm it handles failures gracefully.

In this step, get ready to:
  • Connect the date picker to your fetch function with an event listener.
  • Understand how the loading and error states protect the user experience.
  • Test date navigation and error handling in the browser.
Connect the date picker to your fetch function

Right now, the date picker renders on the page but does nothing when you select a new date. You need an event listener that detects when the user picks a date, then calls fetchAPOD with that date value.

  • In script.js, find the line that says setupDatePicker(); near the bottom of the file.
  • Add the following event listener directly above setupDatePicker();:
datePicker.addEventListener('change', function () {
    fetchAPOD(this.value);
});

What does this code do?

The change event fires whenever the user selects a new date in the date picker input. Inside the listener, this.value gives you the selected date as a YYYY-MM-DD string.

That string gets passed straight into fetchAPOD(), which appends it to the API URL as the date parameter. NASA then returns the photo from that specific day.

  • Save script.js.
  • Refresh your browser and pick a past date in the date picker. The photo, title, and explanation should update to match the date you selected.

Photo not changing when you pick a date?

  • Open the browser console (F12, then the Console tab) and check for error messages. A typo in the event listener will show up here.
  • Make sure the event listener is placed above setupDatePicker(); and below the closing } of the fetchAPOD function.
  • Confirm you typed this.value (not this.date or datePicker.value). Inside the listener, this refers to the date picker element.

Help me debug why my date picker event listener is not working.

Understand the loading and error states

Your fetchAPOD function already includes loading and error handling that protects the user experience. These states work together to keep the interface responsive and informative.

How do the loading and error states work?

When a fetch starts, the function shows the loading message, hides any previous errors or content, and disables the date picker. Disabling the input prevents the user from firing duplicate requests while one is still in progress.

If the fetch succeeds, loading hides and the content appears. If it fails (network error, invalid date, server issue), the catch block shows a friendly message instead of a broken page.

The finally block always re-enables the date picker, whether the request succeeded or failed. This guarantees the user is never stuck with a frozen input.

Test date navigation and error handling

Time to confirm everything works together. You will test both the happy path (loading a past photo) and the failure path (no internet connection).

  • Refresh the page in your browser.
  • Click the date picker and select 2015-07-14 (the day NASA's New Horizons spacecraft flew past Pluto).
  • Confirm the page updates with a new photo, title, and explanation from that date.

What should I see?

You should briefly see the "Loading..." text appear while the API responds. Once loaded, the photo from July 14, 2015 displays with its title and explanation. The date picker remains interactive after the photo loads.

Now test the error handling.

  • Disconnect your computer from the internet (turn off Wi-Fi or unplug Ethernet).
  • Pick a different date in the date picker.
  • Confirm a friendly error message appears instead of a broken page. The message should read: "Failed to load the astronomy photo. Please try again."
  • Confirm the date picker is still enabled after the error (not frozen or grayed out).
  • Reconnect your internet and pick another date to confirm the page recovers.

Not seeing the error message?

  • Make sure you actually disconnected. Some browsers cache recently fetched pages. Pick a date you have not visited yet.
  • Check the browser console for unhandled promise rejections. If you see one, the catch block may have a typo.

Help me figure out why my error state is not showing up.

✔️ Awesome, I've got everything!

Great. Double check you have saved script.js with the event listener in place.

ⓧ I'd like to double check the full code

Here is the complete script.js file. Compare yours against it:

const datePicker = document.getElementById('date-picker');
const loadingEl = document.getElementById('loading');
const errorEl = document.getElementById('error');
const contentEl = document.getElementById('content');
const photoTitle = document.getElementById('photo-title');
const mediaContainer = document.getElementById('media-container');
const dateDisplay = document.getElementById('date-display');
const explanationEl = document.getElementById('explanation');
const copyrightEl = document.getElementById('copyright');

const API_URL = 'https://api.nasa.gov/planetary/apod';
const API_KEY = 'DEMO_KEY';

function getTodayString() {
    const today = new Date();
    return today.toISOString().split('T')[0];
}

function setupDatePicker() {
    datePicker.max = getTodayString();
    datePicker.min = '1995-06-16';
    datePicker.value = getTodayString();
}

async function fetchAPOD(date) {
    loadingEl.style.display = 'block';
    errorEl.style.display = 'none';
    contentEl.style.display = 'none';
    datePicker.disabled = true;

    try {
        let url = `${API_URL}?api_key=${API_KEY}`;
        if (date) {
            url += `&date=${date}`;
        }

        const response = await fetch(url);

        if (!response.ok) {
            throw new Error(`API error: ${response.status}`);
        }

        const data = await response.json();

        photoTitle.textContent = data.title;
        dateDisplay.textContent = data.date;
        explanationEl.textContent = data.explanation;

        if (data.copyright) {
            copyrightEl.textContent = `Image Credit: ${data.copyright}`;
            copyrightEl.style.display = 'block';
        } else {
            copyrightEl.style.display = 'none';
        }

        if (data.media_type === 'video') {
            mediaContainer.innerHTML = `<iframe src="${data.url}" allowfullscreen></iframe>`;
        } else {
            mediaContainer.innerHTML = `<img src="${data.url}" alt="${data.title}">`;
        }

        loadingEl.style.display = 'none';
        contentEl.style.display = 'block';
    } catch (error) {
        loadingEl.style.display = 'none';
        errorEl.textContent = 'Failed to load the astronomy photo. Please try again.';
        errorEl.style.display = 'block';
    } finally {
        datePicker.disabled = false;
    }
}

datePicker.addEventListener('change', function () {
    fetchAPOD(this.value);
});

setupDatePicker();
fetchAPOD();

Your site is now fully interactive. Users can browse NASA's entire archive of astronomy photos going back to 1995, and the page handles failures without breaking. Next up, you will deploy this live to the internet so anyone can use it.

Deploy to Vercel

Your site works locally. It fetches real NASA photos, handles date changes, and shows friendly error messages.

Now it is time to put it on the internet so anyone with the link can see it. You will use Vercel Drop to deploy your site in seconds, no command line or Git required.

In this step, get ready to:
  • Create a free Vercel account.
  • Deploy your site to a live public URL using Vercel Drop.
  • Confirm your site works online and share the link.
Create a free Vercel account

Vercel needs an account before you can deploy. The free Hobby plan is all you need for this project.

  • Open your browser and go to vercel.com/signup.
  • Enter your email address.
  • Vercel will send a 6-digit code to your inbox. Open your email, find the code, and enter it on the Vercel page.
  • Select the Hobby plan when prompted (this is the free tier).
  • You are now signed in to Vercel.

What is Vercel?

Vercel is a platform that hosts websites and makes them accessible to anyone on the internet. It gives you a public URL that points to your files.

Vercel Drop is a feature that lets you deploy by dragging a folder into your browser. No Git, no terminal commands, no build tools needed.

Having trouble signing up?

Check your spam folder for the 6-digit code email. If you still cannot sign up, visit vercel.com/accountrecovery to contact Vercel support.

help me sign up for a Vercel account

Deploy your folder with Vercel Drop

Vercel Drop lets you deploy a static site by dragging your project folder directly onto a web page. Your three files are all it needs.

  • In your browser, go to vercel.com/drop.
  • Open File Explorer (Windows) or Finder (macOS) and navigate to your Desktop.
  • Find the nasa-apod folder.
  • Drag the entire nasa-apod folder onto the Vercel Drop page in your browser.
  • Choose your Vercel team (your personal account) from the dropdown.
  • Enter nasa-apod as the project name.
  • Click Deploy.

What happens when you deploy?

Vercel uploads your three files, detects that there is no framework, and serves them as a static site. Because your folder has an index.html at the root, it automatically becomes your homepage.

The whole process takes just a few seconds. Vercel publishes your files straight to production with a live URL.

Drag and drop not working?

Make sure you are dragging the folder itself (not the individual files inside it). The folder should contain index.html, style.css, and script.js at the top level.

If you see a prompt asking you to choose a homepage, select index.html from the list.

help me deploy my folder to Vercel Drop

Confirm your site is live

After a few seconds, Vercel finishes the deployment and shows you a live URL.

  • Look for the URL that Vercel gives you. It will look something like nasa-apod-xxxx.vercel.app.
  • Click the URL to open your live site in a new tab.
  • Confirm that today's NASA Astronomy Photo of the Day appears with its title and explanation.
  • Pick a different date using the date picker to confirm it still works online.
  • Copy the URL and share it with someone. They can see your site from their own device.

Why does the URL have random characters?

Vercel appends a short random string to make your URL unique across all Vercel projects. Your site is now publicly accessible to anyone with this link.

Site not loading at the URL?

Wait 10-15 seconds and refresh the page. Deployments occasionally take a moment to propagate.

If the page loads but shows no photo, open the browser console (press F12) and check for errors. The NASA API may be temporarily rate-limited if you have been testing heavily.

help me troubleshoot my Vercel deployment

Secret mission

Add a Random Photo Button

Your site lets you pick specific dates, but what about discovering something unexpected? Add a Random Photo button that teleports you to a surprise astronomy image from any day in NASA's 30-year archive.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally and deploys to Vercel's free Hobby plan, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Vercel project on the free Hobby plan (no ongoing cost).
  • Local nasa-apod folder on your Desktop with index.html, style.css, and script.js.

Keep everything running

No action needed. Choose this if you want your NASA APOD site to stay live and accessible at its Vercel URL.

  • Your Vercel deployment stays live indefinitely on the free Hobby plan at no cost.
  • Your local nasa-apod folder remains on your Desktop for future editing.

Pause - I'll come back to this later

Free up system resources while keeping your files and deployment intact for later.

  • Close VS Code to free up memory.
  • Your Vercel deployment stays live at no cost. No need to pause it.
  • When you come back, reopen the nasa-apod folder in VS Code and pick up where you left off.

Delete - I don't want to use this again

Remove all project resources and start fresh.

  • Delete your Vercel project by going to vercel.com/dashboard.
  • Click on your nasa-apod project.
  • Open Settings in the sidebar.
  • Scroll to the bottom of the General page and click Delete in the Delete Project section.
  • Confirm by typing the project name and the phrase delete my project, then click Continue.

Delete your local project files by running this command in your terminal:

macOS

rm -rf ~/Desktop/nasa-apod

Windows

Remove-Item -Recurse -Force ~\Desktop\nasa-apod

That's a Wrap!

That's a Wrap!

Nice work! You just built a live website that pulls real NASA imagery from their public API and deployed it to the internet for anyone to see.

You've learned how to:

  • Build a dynamic website from scratch using HTML, CSS, and JavaScript that fetches live data from NASA's APOD API using async/await.
  • Handle multiple media types from an API response, add interactive date navigation with input validation, and display user-friendly error messages when things go wrong.
  • Deploy a static site live to the internet using Vercel Drop with zero configuration or CLI tools.
  • Secret Mission: Added a Random Photo button that generates a random date between 1995 and today and fetches a surprise astronomy image.

Ready to quiz yourself?