# Santa Tracker's web source is an iframed scene graph with a host in charge

> google/santa-tracker-web is the source of the Google Santa Tracker site, where every game is a self contained page loaded in an iframe and a host layer owns the music, the score and the common UI, connected to the scenes through a small API. The last push was on 2026-10-14 and the released tags stop at 2019, so the repository is best read as a reference implementation rather than a starter you can adopt as is.

**google/santa-tracker-web** — The source code to Santa Tracker's website, by Google

- Repository: https://github.com/google/santa-tracker-web
- Website: https://santatracker.google.com
- Stars: 664 · Forks: 179
- Language: JavaScript
- License: NOASSERTION
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/google-santa-tracker-web

## Every page is a scene loaded inside an iframe

Santa Tracker is split into scenes, and each page of the site corresponds to one of them. The scenes live in the static/scenes/ directory, the main village page among them, and each one is loaded as an iframe. The README describes each scene as relatively self contained, which is the key architectural decision in the codebase: a game is a page, not a module of a page.

That arrangement buys isolation. A scene can be written in whatever style suits it, and the host page does not need to know how the game inside it is built. It also costs a hard boundary, since anything a scene needs from the outside world, such as music or a score, has to be requested rather than reached for.

The host part of the site handles the loading of each scene along with the music and the shared interface elements such as the score display and the tutorial. The two halves talk through an API defined in static/src/scene/api.js. The direction of the call matters: the host notifies scenes of events such as the scene loading, and scenes ask the host to do things like play a song or update the score.

The result is a page where the chrome around the game is consistent and the game itself is disposable. It also means a scene loaded on its own, outside the host, behaves differently, which the development guide comes back to in detail.

## The scene API owns audio, scores and tutorial prompts

The scene API is the contract that makes the split work, and the README describes the shape a new scene has to follow. A scene is created as a folder under static/scenes/ with an index.html that runs code in ES modules only. The page must include a script tag of type module that imports src/scene/api.js, and that import is what sets up the connection to the host.

A scene can then listen for events from the API, and the README names pause, resume and restart as examples of such events. It also describes an api.ready callback that fires when the scene is to be swapped in, which is the moment to set up whatever the scene needs before the user interacts with it.

The host side of the contract covers everything a game should not implement itself. Audio triggers are configured by a scene through api.config({sound: [...]}), and the README notes that this causes all previous audio to stop, which is useful for shutting down the sound of a previous game rather than letting two soundtracks overlap. Tutorial display is described as something a scene requests from the host as well.

Two consequences follow. A scene developed in isolation has no score and no audio of its own, so the only way to test those paths is with the host in place. And because the host and the scene are different documents, the API is a messaging boundary rather than a function call, which is why the events and callbacks are asynchronous in nature.

## Local development needs two ports and a Chromium browser

The development guide is specific enough to be useful. You need yarn or npm, and on Windows you may also need Java, because the binary version of Closure Compiler is not supported on that platform. The steps are to clone the repository, run yarn or npm install to install the dependencies, and then run ./serve.js to start a development server, which copies the development URL to the clipboard.

The serving script listens on two ports by default. Port 8000 serves the host part of the site, matching the production https://santatracker.google.com domain, and port 8080 serves the static content including the scenes. To load a specific scene you open something like http://localhost:8000/boatload.html, and once the site is loaded you can switch scenes programmatically by setting santaApp.route to the scene name in the console.

The npm scripts mirror this workflow, with a start script that runs the serving script and a dev script that delegates to it:

```json
  "scripts": {
    "dev": "npm run start",
    "start": "./serve.js",
    "release": "./release.js",
    "staging-check": "node .cloudbuild/staging-check.js",
    "test": "playwright test",
    "postinstall": "ln -s ../node_modules static/node_modules || true"
  },
```

There is a deliberate detail in the port split. A scene can also be loaded straight from the static domain, for example http://127.0.0.1:8080/st/scenes/elfmaker/, and the guide explains that 127.0.0.1 is intentionally not localhost so that production and static runs stay cross-domain. The README is candid about the cost: the host provides scores, audio and some interface, so not all behaviour is available in that mode.

Development also requires Chrome or a Chromium-based browser as of 2020, because the way ESM import requests are identified depends on extra headers that Chromium sends. The README calls this a bug rather than a feature, which is a fair summary.

## Adding a scene means a folder, an import and three images

The checklist for a new scene in the README is the clearest statement of what a scene really needs. Beyond the index.html with its module import, the api.ready callback and the optional import of the magic URL ./:closure.js for Closure-style code, there are image assets with fixed dimensions: a scene name PNG at 950x564 and a 475x282 version under static/img/scenes, plus an open graph image of 1333x1000 under prod/images/og. The scene also has to be named inside static/src/strings/scenes.js so that it appears in the interface, and if it should not reach production it can be disabled inside release.js.

That last point is worth noting for anyone reading the repository as a product. The release process has a per-scene switch, so the code contains scenes at different levels of readiness, and the tree tells you which ones were shipped.

The whole path from clone to running game is short because the build system does the work. The build provides a virtual file system that compiles source types on demand, so you can request a compiled artefact that does not exist on disk yet. In JavaScript that includes rewriting non-relative imports for node_modules, so an import of LitElement is resolved to its full path for development and for release. The dependency list backs that up, with lit-element, rollup, babel, sass, google-closure-compiler, three and lottie-web among the packages in package.json.

## A virtual file system compiles SCSS and JSON5 on demand

The environment section of the README describes what the build generates for you. CSS files are generated for their corresponding SCSS, JSON for the corresponding JSON5, and the static/scenes/sceneName/:closure.js file can be read to compile an older scene's js folder with Closure Compiler and produce a JavaScript module with a default export. None of these files exist on disk; they are created when used. The stated example is that if foo.scss exists, loading foo.css compiles it automatically.

Alongside that, the import system accepts module types that are not native to browsers, meaning CSS, JSON and HTML can be imported alongside JavaScript. There is a Sass helper for that layer too: _rel(path.png) generates a url() pointing at a file relative to the current SCSS source, including when the SCSS is imported elsewhere, and it works whether the result ends up behind a link tag or inside a Web Component.

JavaScript has its own helpers in static/src/magic.js, in the form of template tag functions that are real functions at authoring time but inlined at release time. One generates the matching internationalisation string for a message id, another generates an absolute reference to a file within the static tree.

The practical consequence is that a contributor rarely needs to know the release build. You write SCSS, import CSS, and the pipeline produces the artefacts. The cost is that the mapping between what you edit and what ships is not visible in the tree, and anyone debugging a production bundle needs to understand the virtual file system to know where a given file came from.

## Contributions are closed and the audio engine comes from a vendor

Two limits in the README are practical barriers for anyone hoping to build on this code.

The first is the contribution policy. The project states that in general it does not accept external contributions from the public, and that bug reports and feature requests can be filed, or the engineering lead contacted. A repository that is published for reading rather than for contribution changes how you should use it. Pull requests are not the path in, so if you need a change, you are forking it.

The second is the audio layer. The README describes an audio library that exists in the production host only, and that can be triggered by API calls from scenes. It is largely undocumented and provided by an external vendor. The source files for the sounds are in the repository under static/audio, licensed as CC-BY, but the engine that plays them is not something you can study here.

Translations are a third boundary of the same kind. The site carries translations for a range of languages sourced from Google's internal translation tool, and the README says that to add a string for development you modify en_src_messages.json and ask a Google employee to request a translation run. The full text of that section is cut off in the copy at hand, but the instruction is clear enough: new strings do not get translated automatically by anyone outside the company.

Input handling has one more nuance worth knowing if you plan to write a scene. The README asks that touch, keyboard and gamepad input be supported where possible, and notes that basic gamepad support is offered through synthetic keyboard events in static/src/core/keys.js.

## The branch moved in 2026 while the released tags stopped in 2019

Maintenance signals here are mixed, and the gap between them is the point. The repository is not archived, and the last push was on 2026-10-14, so the code is still being touched. The releases, though, are annual themes rather than a version history, with the newest tagged 2019 (Holly) on 2020-11-27, followed by 2016 (Elves) on 2017-03-08 and 2015 (Dasher) on 2016-07-30. There is no tag describing the current state of the branch.

The package manifest adds a data point. The version field in package.json reads 2021.1.0, which sits between the 2019 release and the 2026 push, so the manifest version tells you nothing about what is on main.

Licensing is the other area that needs reading rather than assumption. The repository's LICENSE file is a custom licence that GitHub cannot classify, while the license field in package.json declares Apache-2.0, and the audio files under static/audio are licensed CC-BY. Those three statements do not obviously agree, so anyone copying code out of this repository should read the LICENSE file directly and decide for themselves what the terms require. This is a description of the situation rather than legal advice.

For most readers the honest conclusion is that this is a reference implementation with a long life behind it. The scene and host architecture, the API contract and the on-demand compilation are worth studying. Reusing the code wholesale in a product you maintain is a different question, and the closed contribution policy means the upstream project will not help you carry that.

## Conclusion

Read santa-tracker-web if you want to understand how a large, self contained interactive site can be split into host and iframe scenes, or if you need to maintain a fork of the holiday experience. Do not adopt it as a base for your own product: the project states that external contributions are not accepted, the released tags end at 2019 even though the branch moved on 2026-10-14, and the licence is a custom one that GitHub cannot classify. Verify first by running ./serve.js on a Chromium browser and loading http://localhost:8000/boatload.html, then check the LICENSE file directly before copying any of it into work of your own.

## FAQ

### How do I run Google Santa Tracker locally?

Clone the repository, run yarn or npm install, then run ./serve.js to start the development server, which copies the URL to the clipboard. The server listens on port 8000 for the host and port 8080 for the static scenes, and a specific scene can be opened at a URL such as http://localhost:8000/boatload.html.

### What is the relationship between the host and the scenes in santa-tracker-web?

The host loads each scene, which is a self contained page in an iframe, and owns the music, the score and the common interface. The two communicate through static/src/scene/api.js, where the host notifies scenes of events such as loading, pause, resume and restart, and scenes request sounds, score updates and tutorials in return.

### Can I contribute changes to Google Santa Tracker?

No, not directly. The README states that the project generally does not accept external contributions from the public, and that bug reports, feature requests or contact with the engineering lead are the routes offered instead. Forking is the practical option for anyone needing changes.

## Sources

- [google/santa-tracker-web on GitHub](https://github.com/google/santa-tracker-web)
- [Issues](https://github.com/google/santa-tracker-web/issues)
- [Project website](https://santatracker.google.com)
- [README](https://github.com/google/santa-tracker-web/blob/main/README.md)
- [Releases](https://github.com/google/santa-tracker-web/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/google-santa-tracker-web
