Skip to content

Store hours script (storeHours.js)

Staff changing hours, store details or holiday closures should use Update store hours, store details and holiday closures. This page is the developer reference for the script behind it.

js/storeHours.js answers two questions for the browser: what are a store’s details, and is it open right now. It reads two blocks the footer prints on every page, one built from the Store Finder records and one from the Operating_Exceptions custom config. The public entry points are isStoreOpen(), getStoreConfig() and hydrateStoreFields().

Where the data comes from

SourceFeedsFormat
Store Finder standard fields (stloc_name, stloc_street1, stloc_city, stloc_state, stloc_zip, stloc_phone, stloc_email, stloc_lat, stloc_lng, store_id)Name, address, phone, email, coordinates, the object keyAs typed. stloc_city is stored in capitals by the platform
Store Finder custom field misc1 / misc2 (cPanel “weekdays open” / “weekdays close”, Short Text)Monday to Friday open / closeHH:MM, 24-hour
Store Finder custom field misc3 / misc4 (cPanel “Saturday open” / “Saturday close”, Short Text)Saturday open / closeHH:MM, 24-hour
Store Finder custom field misc5 (added 2026-09-23)slug, the /locations/<slug> pageLower-case slug
CONFIG:Operating_ExceptionsstoreHours.closing_exceptions, site-wide closuresJavaScript object literal, not JSON

There are no Sunday fields. Sunday is hard-coded as closed for every store. The Store Finder allows at most five custom fields per store, so all are in use. Every custom field value is cut silently to 50 characters by the platform, whatever the field type; the cPanel field form doesn’t state the limit. Because stloc_city can’t be changed from capitals in cPanel, the shopper-facing templates wrap it in .store-city, which css/style.css displays with only the first letter in capitals.

The four records are:

store_idStoremisc5 (live, 2026-10-21)
"1"Altapac Kotarakotara
"2"Altapac Maitlandmaitland
"3"Altapac Artarmonartarmon
"4"Altapac Burwoodburwood

footers/includes/scripts.template.html prints both blocks, then loads the script with defer:

<script type="application/json" id="altapac-stores">{ ...one member per store..., "_": null }</script>
<script>
const storeHours = { "closing_exceptions": [@CONFIG:Operating_Exceptions@] };
</script>
<script defer src="[%ntheme_asset%]js/storeHours.js[%/ntheme_asset%]"></script>

The #altapac-stores JSON block

The block is built with [%list type:'store_location' limit:'50'%]. It holds one member per Store Finder record, keyed by store_id. This is the live Artarmon member on 2026-10-21:

"3": {
"store_name": "Altapac Artarmon",
"slug": "artarmon",
"phone": "02 8999 8878",
"email": "artarmon@altapac.com",
"address": "Unit 13/87 Reserve Rd, ARTARMON NSW 2064",
"lat": "-33.8154910000",
"lng": "151.1873400000",
"openingHours": {
"weekdays": { "open": "07:30", "close": "17:00" },
"saturday": { "open": "09:00", "close": "15:00" },
"sunday": { "closed": true }
}
}
  • Every value is a string, including lat and lng. Callers parseFloat them.
  • address is assembled as street1, CITY STATE ZIP. There is no street2.
  • An empty custom field prints as "". The script treats an empty open or close time as closed.
  • Each member ends with a comma. The final "_": null member absorbs the last one. getStoreIds() skips any key starting with _.
  • It’s a type="application/json" block, so the browser never runs it. A bad value can only make JSON.parse fail. It can’t break other scripts on the page.

The storeHours global

The inline script declares const storeHours with only closing_exceptions. The store members are merged into it later, on the first call to any reader (see Load order). closing_exceptions is never overwritten by the JSON block.

Operating_Exceptions is interpolated unquoted, so it must be a valid JavaScript expression. The live value is an object with single-quoted keys and values and a trailing comma:

{
'2026-04-03': 'Good Friday',
'2026-06-08': 'King\'s Birthday',
'2026-12-25': 'Christmas Day',
}

Both the single quotes and the trailing comma are invalid JSON, which is why this block stays out of #altapac-stores and must never go through JSON.parse.

Load order

  1. The parser reaches the JSON block and the inline storeHours script.
  2. storeHours.js is deferred, so it runs after parsing and before DOMContentLoaded. Its last lines call hydrateStoreFields() straight away (or on DOMContentLoaded if the document is somehow still loading).
  3. The contact modal and the product availability panel call isStoreOpen() and getStoreConfig() at or after DOMContentLoaded.

loadStoreData() sets its storeDataLoaded latch on the first call, whether or not it found the block. That’s safe only because the JSON block comes before the deferred script in the same footer file. If you move the block below the script, or drop defer, a missing block on the first call becomes permanent for the page.

DEFAULT_STORE_ID

var DEFAULT_STORE_ID = "1";

isStoreOpen() uses it when the argument is undefined, null or "". No caller in the theme relies on it. Every caller passes an id. One subtle path: isStoreOpen(el.getAttribute('data-store-id')) on an element without the attribute passes null, and gets Kotara’s status rather than false.

Public functions

Everything is a global function on window. The file isn’t a module.

FunctionReturnsNotes
isStoreOpen(storeId?)true or falseIs that store open now, in Sydney time. See How isStoreOpen decides
getStoreConfig(storeId)The store’s object from the JSON block, or nullnull if the id is falsy, unknown, or the store data didn’t load. Requires an openingHours member
getTodayHours(storeId){ open, close }, { closed: true }, or nullToday’s hours by Sydney weekday. { closed: true } if either time is empty. null if the store isn’t found. Ignores closing exceptions. No theme caller
getStoreIds()Array of id strings, e.g. ["1","2","3","4"]Skips closing_exceptions and _-prefixed keys. [] if storeHours is missing. No theme caller
isClosingException(dateStr?, exceptions?)true or falseIs dateStr (YYYY-MM-DD, default today in Sydney) in the closures list. exceptions overrides storeHours.closing_exceptions
hydrateStoreFields(root?)NothingFills data-store-field elements under root (default document). See Store detail tags
formatStoreHours(day)A string such as "7:30am – 5pm", or "Closed"day is { open, close } or { closed: true }. "Closed" if either time is missing or unparsable
formatStoreTime(hhmm)A string such as "7:30am" or "5pm", or nullDrops :00. "12:00" gives "12pm", "00:30" gives "12:30am"

Internal helpers, also global: loadStoreData(), getOpeningHoursForStore(storeId) (with a fallback to a legacy top-level storeHours.openingHours that nothing sets any more), getClosingExceptionsList(), timeStringToMinutes(), getCurrentAEST() and formatDateYYYYMMDD(). formatDateDDMMYYYY() is left over from the old single-store version and nothing calls it.

How isStoreOpen decides

In order:

  1. Resolve the id, falling back to DEFAULT_STORE_ID.
  2. Look up that store’s openingHours. None, so false. This covers an unknown id and store data that failed to load.
  3. If today’s Sydney date is in closing_exceptions, false. Closures apply to every store.
  4. Pick sunday (always { closed: true }), saturday or weekdays by Sydney weekday. Closed, so false.
  5. Convert open and close to minutes. If either is empty, false.
  6. true if the current Sydney minute is between open and close, inclusive at both ends. A store that closes at 17:00 still reads as open during the 17:00 minute.

”Sydney time” comes from getCurrentAEST(), which formats the current instant in Australia/Sydney and rebuilds a Date from the parts. Only its weekday, hours and minutes are read, so daylight saving is handled by Intl.

timeStringToMinutes() splits on : and nothing more. 08:00 and 8:00 both work. 8am, 8.00 and 5pm give NaN, every comparison fails, and the store reads as closed. formatStoreTime() returns null for the same values, so formatStoreHours() prints "Closed". The server-side templates print the raw text whatever it is, so a bad value shows as typed on the home page and contact page while the script calls the store closed.

Operating_Exceptions parsing

getClosingExceptionsList() accepts either shape:

  • an object, where only the keys are used (the live format)
  • an array of date strings, such as ['2026-12-25', '2027-01-01']

Anything else gives an empty list. The holiday names are never read or displayed.

isClosingException() does an exact string match against formatDateYYYYMMDD() of the Sydney date. Keys must be YYYY-MM-DD with leading zeros. 2026-12-5 and 25/12/2026 never match, so the stores read as open on that day.

To check a change to the config, open any product page with a Pick up tab. Store names mean the store data loaded. “Store 1” to “Store 4” means it didn’t. This check works at any hour, unlike the open or closed status.

Store detail tags (hydrateStoreFields)

hydrateStoreFields() lets CMS content show Store Finder values instead of hand-typed copies. It runs once when storeHours.js loads, over every [data-store-field] element in the document.

<span data-store-id="3" data-store-field="phone">02 8999 8878</span>
<a data-store-id="1" data-store-field="phone" href="tel:0249414444">Call the Kotara store</a>
  • data-store-id is passed to getStoreConfig(), so it must be "1" to "4". A slug such as kotara, or a missing attribute, matches nothing and the element is left alone.
  • data-store-field picks the value:
data-store-fieldValueExample
namestore_nameAltapac Artarmon
phonephone, as typed02 8999 8878
emailemailartarmon@altapac.com
addressaddressUnit 13/87 Reserve Rd, ARTARMON NSW 2064
weekdaysformatStoreHours(openingHours.weekdays)7:30am – 5pm
saturdayformatStoreHours(openingHours.saturday)9am – 3pm
sundayformatStoreHours(openingHours.sunday)Closed

How the element is changed:

  • Not a link: textContent is replaced. Any child elements, such as an icon inside the span, are removed.
  • A link (<a>): only href changes, and the link text is kept. phone sets tel: with every character except digits and + stripped. email sets mailto:. Other fields do nothing on a link. Put a span tag inside the link to show the value too.
  • Fallback: the existing text or href stays when the store isn’t found, the store data didn’t load, the field name is unknown, or the value is empty.

The hours fields never come back empty while the store exists, because formatStoreHours() returns "Closed". So clearing a store’s Saturday fields replaces the fallback text with “Closed” rather than leaving it. address is never empty either, since it always contains the commas and spaces.

Closing exceptions don’t affect these tags. On a holiday they still show the normal weekly hours.

The function runs once. Content added to the page later, such as an AJAX-loaded panel, isn’t filled unless you call hydrateStoreFields(newRoot) yourself.

On 2026-10-21 the tags are used in Altapac-Content under pages/locations/ (the four location pages and the Artarmon and Burwood bullbars, canopies, roof racks and towbars sub-pages), for weekdays, saturday, sunday and phone.

Consumers

These call the script in the browser:

ConsumerCallsShowsWhen the store data is missing
Contact modal, every page (footers/includes/contactModal.template.html)isStoreOpen(id) for each .contact-store[data-store-id] row, on DOMContentLoaded”open now” or “closed” per store. The Call button is enabled when any store is open, and dials CONFIG:COMPANY_PHONE, not a store numberEvery store “closed”, Call stays disabled
Product page Pick up panel (products/includes/availability.template.html)getStoreConfig(id) for store_name, slug, lat, lng. isStoreOpen(id) for the nearest store holding stockLinked store names, distance from the postcode, “Ready today at” when that store is open, otherwise “Ready at”Labels read “Store 1” to “Store 4”, no links, no distance sort
CMS content tagshydrateStoreFields(), via getStoreConfig(id)See Store detail tagsFallback text stays

The Pick up panel maps warehouses to stores in its own STORES[] array: warehouses 4 and 9 to store "1", 7 to "2", 11 to "3" and 16 to "4". Both callers guard with typeof ... === 'function' and a try, so a missing script degrades to the “no data” branch.

These read the same Store Finder fields server-side, with their own [%list type:'store_location'%] loop. They don’t use storeHours.js and ignore closing exceptions:

TemplateUses
Home page store carousel (cms/home.template.html)Name linked to /locations/[@misc5@], address, misc1–misc4 printed raw (08:00 - 17:00), store logo as the slide background
Contact page “Visit Our Showrooms” (cms/contact.template.html)Map, name, address, phone, email, raw hours, “Find Out More” link from misc5
Contact modal store rows (footers/includes/contactModal.template.html)Name, address, raw hours, phone, email. The script only fills the status marker
Structured data (headers/includes/structured_data/structured_data.template.html)One LocalBusiness node per store. openingHoursSpecification only if misc1 and misc2 are both set, with Saturday added only if misc3 and misc4 are both set. @id and url from misc5, falling back to the /d/ page. Image, description and rating are hard-coded per store_id
Store Finder pages /d/<name>/<id> (stockists/template.html, headers/includes/head.template.html)Redirect to /locations/[@misc5@], and canonical to the same page (not live yet, AUD-83; on altapac.com today the canonical points at the /d/ page itself)

So the raw text on the home page, contact page and modal can disagree with the script’s view. For example, 8am prints as typed but the script reads the store as closed.

Failure modes

CauseScript resultVisible symptom
" or \ in any Store Finder fieldJSON block fails to parse, no stores”Store 1” to “Store 4” on product pages, every store closed in the modal, tags keep fallback text, invalid JSON-LD
Operating_Exceptions empty or not valid JavaScriptstoreHours undeclared, no stores, no closuresSame as above, plus a console SyntaxError
Closure date not YYYY-MM-DDNever matchesStores read as open on the holiday
misc1–misc4 not H:MM or HH:MMThat day reads closedModal says closed in trading hours. Raw text prints elsewhere
One of a pair of hour fields emptyThat day reads closedHalf a range such as 09:00 - on the home and contact pages
misc1 or misc2 emptyWeekdays closedStore’s openingHoursSpecification dropped entirely, including Saturday
data-store-id is a sluggetStoreConfig() returns nullTag keeps fallback text, which goes stale

Changing the script

  • Keep the JSON block above the deferred storeHours.js in the same footer file, and keep defer. See Load order.
  • Never move Operating_Exceptions into the JSON block or behind JSON.parse. The live value isn’t JSON. Hardening the empty-config bug means either converting the config to strict JSON first or giving the interpolation a default.
  • A new store gets its JSON member, carousel slide, contact card and modal row automatically. It also needs a STORES[] row in availability.template.html and its own SEO arm in the structured data. The contact page tabs and enquiry forms are hand-built and need changing too.
  • node scripts/storehours-check.cjs in Altapac-ReDev runs storeHours.js in a bare VM and asserts the contract the templates rely on.

Source files

Altapac-ReDev, branch development (77cdf1b), checked 2026-10-21. The live production theme’s storeHours.min.js matched this source on the same day, including hydrateStoreFields.

  • src/js/storeHours.js
  • src/templates/footers/includes/scripts.template.html (JSON block lines 33–53, storeHours global line 57, script tag line 60)
  • src/templates/footers/includes/contactModal.template.html
  • src/templates/products/includes/availability.template.html (STORES[] and storeData() around lines 269–280, isOpenNow() around line 692)
  • src/templates/headers/includes/structured_data/structured_data.template.html
  • src/templates/cms/home.template.html, src/templates/cms/contact.template.html
  • src/templates/stockists/template.html, src/templates/headers/includes/head.template.html
  • scripts/storehours-check.cjs
  • docs/map/store-data/StoreData.md, docs/map/store-data/issues.md, docs/records/2026-09-15-store-finder-store-data/spec.md, docs/probes/RESULTS.md (Store Finder probe)
  • .claude/skills/base-templating/references/value-formats.md (Operating_Exceptions section)