I maintain mabims.dev, a free, open-source Hijri calendar API for Indonesia. Its calendar page, Kalender Hijriah MABIMS, now ranks #1 on Google and Bing for "kalender hijriah mabims" (screenshots below, taken 21 Sep 2026).
Some context first. It's a niche, low-competition query, rankings vary by location, and I can't prove which change moved the needle. This is what the page does today.
Real HTML on every request
The page started as a static build. It's now rendered on the server, in a Node container separate from the API. The file opts out of prerendering with export const prerender = false;, and on each request it fetches from the API over the internal network:
const apiBase = import.meta.env.INTERNAL_API_BASE ?? 'http://localhost:8000';
const res = await fetch(`${apiBase}/api/v1/month?year=${currentYear}&month=${currentMonth}&calendar=hijri`);
The month grid, today's date, important dates, a 12-month summary, and the hilal facts are all in the first HTML response. The browser script only handles month navigation and refreshes. The browser uses the public API base, and the server uses the internal one.
Fresh, but cheap: cache until midnight Jakarta
Rendering on every hit would be wasteful, and a daily static build goes stale. So the page sets its own cache header, expiring at midnight in Jakarta, the same rhythm as the API's /today:
const ttl = 86400 - secsOfDayInJakarta;
Astro.response.headers.set('Cache-Control', `public, max-age=0, s-maxage=${ttl}`);
The CDN (Bunny) can hold the page until then, so the origin should render at most once per region per day. The header is only set when the /today fetch succeeded, so a failed render isn't cached for the day.
What's in the HTML
-
A real table for the year. A 12-month overview with
<th scope="row">for each month, its Gregorian date range, and its length in days. - Text generated from data. The hilal section builds a sentence like "Kapan Jumadil Awal dimulai? Berdasarkan kriteria Neo MABIMS…", followed by the actual numbers: moon altitude, elongation, illumination, moon age, sunset and moonset.
- Images with descriptive alt text, lazy-loaded, with width and height set.
-
Important dates as
<article>rows, not a JS-only widget.
One source for the FAQ and its schema
The visible FAQ and the FAQPage JSON-LD come from the same array:
acceptedAnswer: { '@type': 'Answer', text: f.aPlain ?? f.a }
Answers with links or <code> have aHtml for display and aPlain for the schema. One question, the date of 1 Ramadhan, is built from live event data.
The title comes from data
const title = `Kalender Hijriah MABIMS ${currentYear} (${masehiYear}) • Kriteria Kemenag Neo MABIMS`;
The target phrase is first, the years roll over automatically, and there's an explicit canonical. Google still shows my old title, "Kalender Hijriah 1448 H & 2026", while Bing shows the new one. That's recrawl lag, so I requested reindexing in Search Console.
Say what you are
The page says in several places that it's independent, not affiliated with Kemenag, and that results are estimates. The FAQ also explains why Google Calendar (Umm al-Qura) can differ by a day. For a topic people take seriously, I think that honesty builds trust, and it answers questions people are already searching.
What I'd still fix
- Midnight vs maghrib. The cache expires at midnight, but the Hijri date changes at maghrib.
- The month grid is
divs, not a semantic table. -
No Event schema or
<time>tags for the holidays, though the data is there. - The H1 is followed by only a date line. The explanatory text sits lower, in the "Tentang" section.
Beyond the page
I also published posts on dev.to, Medium, and Kompasiana, and the GitHub READMEs link to the page with descriptive anchors.
If you've ranked a data or tool page, I'd like to hear what mattered most for you.
Peace!


Top comments (0)