Daniel Morgan

Moon Phase Helper Tool

This post was published on a day the moon looked like:
Moon Phase
I made a quick lil service to show an emoji of the Moon based on the current date for the fantastic Dark Properties newsletter and thought I'd share more widely.

The format for this service is: https://dark.properties/moonphase/moonphase/01-25-2024.png so it works well with any platform where you can programmatically set MMDDYYY, or in our case: Sendy on a PHP server. It will essentially give you the day's phase. An example of using this in a sendy template: https://dark.properties/moonphase/moonphase/[currentmonthnumber]-[currentdaynumber]-[currentyear].png

You can also load: https://dark.properties/moonphase/moonphase/today.png

It just requires png files in the moon_phases directory with these filenames:


You can test this using the Dark Properties server, which has a moonphase directory with the files in it, simply use:

<img src="https://dark.properties/moonphase/moonphase/[currentmonthnumber]-[currentdaynumber]-[currentyear].png" width="20" height="20">

... In your Sendy email. Here's a gist to set this up yourself. (Don't rely on the dark.properties service, it may go down)

Here's how to set this up yourself:

Start with a url handler in the form of a htaccess rewrite rule in the 'moonphase' directory on your server:

# place this in the folder 'moonphase' at the top level directory to load

RewriteEngine On

# Check if mod_rewrite is enabled
<IfModule mod_rewrite.c>
    # Rewrite only if the request is for a moon phase image
    RewriteRule ^moonphase/([0-9]{2})-([0-9]{2})-([0-9]{4})\.png$ moonphase.php?month=$1&day=$2&year=$3 [L,QSA]

Here's the main event, which loads your phases based on the current 'phase'. The incredible Suncalc helps us load the phases based on the date provided:


// Centralized response handling
function sendHttpResponse($code, $message = '', $contentType = 'text/plain') {
    header("HTTP/1.1 $code");
    header("Content-Type: $contentType");
    if (!empty($message)) {
        echo $message;

// Error logging function
function logError($message) {
    // Assuming a writable directory 'logs' exists. Adjust the path as needed.
    error_log($message . "\n", 3, __DIR__ . '/logs/error_log.txt');

require_once 'suncalc.php';

use AurorasLive\SunCalc;

function getMoonPhaseImage($year, $month, $day) {
    $dateString = "$year-$month-$day";
    $date = new DateTime($dateString);
    // Specify your latitude and longitude values
    $lat = 40.821287; // Example latitude
    $lng = -73.923168; // Example longitude

    // Create an instance of SunCalc with the date, latitude, and longitude
    $sunCalc = new SunCalc($date, $lat, $lng);

    // Calculate the moon illumination for the given date
    $moonIllumination = $sunCalc->getMoonIllumination();

    // Determine the moon phase based on the phase value
    $phase = $moonIllumination['phase'];
    if ($phase < 0.03) {
        return 'new_moon.png';
    } elseif ($phase < 0.22) {
        return 'waxing_crescent.png';
    } elseif ($phase < 0.28) {
        return 'first_quarter.png';
    } elseif ($phase < 0.47) {
        return 'waxing_gibbous.png';
    } elseif ($phase < 0.53) {
        return 'full_moon.png';
    } elseif ($phase < 0.72) {
        return 'waning_gibbous.png';
    } elseif ($phase < 0.78) {
        return 'last_quarter.png';
    } elseif ($phase < 0.97) {
        return 'waning_crescent.png';
    } else {
        return 'new_moon.png';

$month = $_GET['month'] ?? null;
$day = $_GET['day'] ?? null;
$year = $_GET['year'] ?? null;

// More robust date validation
if (!preg_match('/^\d{2}-\d{2}-\d{4}$/', "$month-$day-$year") || !checkdate($month, $day, $year)) {
    sendHttpResponse(400, 'Invalid date provided.');

$moonPhaseImage = getMoonPhaseImage($year, $month, $day);
$imagePath = __DIR__ . '/moon_phases/' . $moonPhaseImage;

if (file_exists($imagePath)) {
    header('Content-Type: image/png');
    // Set cache control: max-age is in seconds (example here is for one year)
    header('Cache-Control: public, max-age=31536000');
    // Set Expires header for 1 year in the future
    header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 31536000) . ' GMT');
} else {
    logError('Moon phase image not found for date: ' . "$year-$month-$day");
    sendHttpResponse(404, 'Moon phase image not found.');

If you front your server with Cloudflare, they will kindly cache responses for you for a year.

If you prefer Node, here's that version.

by Daniel Morgan, tagged with: