{"id":982,"date":"2026-07-24T13:25:00","date_gmt":"2026-07-24T13:25:00","guid":{"rendered":"https:\/\/eugigufo.net\/en\/?p=982"},"modified":"2026-04-23T12:06:34","modified_gmt":"2026-04-23T12:06:34","slug":"annual-blog-calendar","status":"publish","type":"post","link":"https:\/\/eugigufo.net\/en\/blog\/982\/","title":{"rendered":"Annual blog calendar"},"content":{"rendered":"<p>For quite some time, I&nbsp;had been meaning to&nbsp;create something like this on&nbsp;my&nbsp;website: an&nbsp;annual calendar (both for the current year and for each of&nbsp;the past years) in&nbsp;which, for every date, there is&nbsp;a&nbsp;link leading to&nbsp;a&nbsp;collection of&nbsp;blog posts published on&nbsp;that very day (if&nbsp;anything was actually published). On&nbsp;LiveJournal (and other blogging platforms), such a&nbsp;calendar existed and was very convenient as&nbsp;a&nbsp;tool for finding old articles and\/or analysing blog activity (one&rsquo;s own or&nbsp;that of&nbsp;others). In&nbsp;WordPress, however, such a&nbsp;calendar does not exist and, if I&nbsp;remember correctly, it&nbsp;never has (or&nbsp;perhaps it&nbsp;did exist so&nbsp;long ago that I&nbsp;have completely forgotten about&nbsp;it): there is&nbsp;only a&nbsp;similar calendar based on&nbsp;individual months, which is&nbsp;decidedly inconvenient for most searches and analyses.<br \/>\nTo&nbsp;create something like this on&nbsp;my&nbsp;own, I&nbsp;needed a&nbsp;bit of&nbsp;free time: at&nbsp;last, I&nbsp;managed to&nbsp;find&nbsp;it! Even more importantly (and interestingly), that time was not wasted: I&nbsp;succeeded in&nbsp;creating the&nbsp;calendar archive. A&nbsp;working example is&nbsp;available via <a href=\"https:\/\/eugigufo.net\/en\/blog\/archives\/calendar\/\">the link below<\/a>, while in&nbsp;this article&nbsp;I will describe all the features of&nbsp;my&nbsp;calendar and provide the basic PHP and CSS code.<br \/>\nFeatures of&nbsp;my&nbsp;annual blog calendar for WordPress:<br \/>\n&mdash;&nbsp;full-year calendar (all 12&nbsp;months are visible, regardless of&nbsp;the&nbsp;current date at&nbsp;the time of&nbsp;viewing);<br \/>\n&mdash;&nbsp;the ability to&nbsp;view past years (though not earlier than the year of&nbsp;the&nbsp;blog&rsquo;s first public post);<br \/>\n&mdash;&nbsp;days with published posts are automatically turned into links leading to&nbsp;the corresponding dates in&nbsp;the article archive;<br \/>\n&mdash;&nbsp;when hovering over a&nbsp;linked day, a&nbsp;tooltip appears showing the&nbsp;number of&nbsp;posts published on&nbsp;that date;<br \/>\n&mdash;&nbsp;in&nbsp;calendars for past years, the current date (&laquo;today&raquo;) is&nbsp;highlighted with a&nbsp;clearly visible border;<br \/>\n&mdash;&nbsp;cells corresponding to&nbsp;days on&nbsp;which at&nbsp;least one post was published change their background colour;<br \/>\n&mdash;&nbsp;the calendar features a&nbsp;responsive design: on&nbsp;large and medium screens, a&nbsp;3&times;4&nbsp;grid of&nbsp;months is&nbsp;displayed, while on&nbsp;smaller screens the&nbsp;months are arranged in&nbsp;a&nbsp;column;<br \/>\n&mdash;&nbsp;database queries are optimised so&nbsp;that the&nbsp;calendar loads quickly even on&nbsp;blogs with tens of&nbsp;thousands of&nbsp;posts.<br \/>\nAnd now we&nbsp;move on&nbsp;to&nbsp;the calendar code (I&nbsp;hope you have read all the text above&mdash;it may help you find your way around).<br \/>\nThe PHP code, which you can test directly as&nbsp;it&nbsp;is: simply copy and paste it&nbsp;into your page layout:<\/p>\n<pre class=\"brush: php; collapse: false; title: listing 1; notranslate\" title=\"listing 1\">\n&lt;?php\nglobal $wpdb;\n\n\n\/* ======================================\n   1. THE FIRST YEAR OF BLOG\n====================================== *\/\n$first_post_date = $wpdb-&gt;get_var(&quot;\nSELECT post_date\nFROM $wpdb-&gt;posts\nWHERE post_status=&#039;publish&#039;\nAND post_type=&#039;post&#039;\nORDER BY post_date ASC\nLIMIT 1\n&quot;);\n\n$first_year = date(&#039;Y&#039;, strtotime($first_post_date));\n$current_year = date(&#039;Y&#039;);\n$year = isset($_GET&#x5B;&#039;yr&#039;]) ? intval($_GET&#x5B;&#039;yr&#039;]) : $current_year;\n\nif ($year &lt; $first_year) $year = $first_year;\nif ($year &gt; $current_year) $year = $current_year;\n\n\n\/* ======================================\n   2. POST DATES (OPTIMISED SQL QUERY + CACHE)\n====================================== *\/\n$cache_key = &#039;calendar_&#039;.$year;\n$post_dates = get_transient($cache_key);\nif ($post_dates === false){\n\t$post_dates = &#x5B;];\n\t$results = $wpdb-&gt;get_results($wpdb-&gt;prepare(&quot;\n\tSELECT DATE(post_date) as post_day, COUNT(ID) as total\n\tFROM $wpdb-&gt;posts\n\tWHERE post_status=&#039;publish&#039;\n\tAND post_type=&#039;post&#039;\n\tAND YEAR(post_date)=%d\n\tGROUP BY post_day\n\t&quot;, $year));\n\tforeach ($results as $row){\n\t\t$post_dates&#x5B;$row-&gt;post_day] = $row-&gt;total;\n\t}\n\tset_transient($cache_key,$post_dates,12*HOUR_IN_SECONDS);\n\t}\n\n\n\/* ======================================\n   3. BROWSE BY YEAR\n====================================== *\/\necho &#039;&lt;div class=&quot;calendar-nav&quot;&gt;&#039;;\nfor ($y=$first_year; $y&lt;=$current_year; $y++){\n\tif ($y==$year){\n\t\techo &#039;&lt;span class=&quot;current-year&quot;&gt;&#039;.$y.&#039;&lt;\/span&gt;&#039;;\n\t}else{\n\t\techo &#039;&lt;a href=&quot;?yr=&#039;.$y.&#039;&quot;&gt;&#039;.$y.&#039;&lt;\/a&gt; &#039;;\n\t}\n}\necho &#039;&lt;\/div&gt;&#039;;\n\n\n\/* ======================================\n   4. \u2018TODAY IN THE PAST YEARS\u2019 COMPARISON\n====================================== *\/\n$today_month = date(&#039;m&#039;);\n$today_day = date(&#039;d&#039;);\n$today_key = $year.&#039;-&#039;.$today_month.&#039;-&#039;.$today_day;\n$today_has_posts = isset($post_dates&#x5B;$today_key]);\n\n\n\/* ======================================\n   5. YEAR CONTAINER\n====================================== *\/\necho &#039;&lt;div class=&quot;calendar-year&quot;&gt;&#039;;\n\n\n\/* ======================================\n   6. MONTHLY CYCLE\n====================================== *\/\nfor ($month=1; $month&lt;=12; $month++) {\n\techo &#039;&lt;div class=&quot;calendar-month-block&quot;&gt;&#039;;\n\t$month_name = date_i18n(&#039;F&#039;, mktime(0,0,0,$month,1,$year));\n\t$month_has_posts = false;\n\tforeach ($post_dates as $date =&gt; $count){\n\t\tif (strpos($date,$year.&#039;-&#039;.sprintf(&#039;%02d&#039;,$month))===0){\n\t\t\t$month_has_posts = true;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\/* month title *\/\nif ($month_has_posts){\n\techo &#039;&lt;h3 class=&quot;month-title&quot;&gt;\n\t&lt;a href=&quot;&#039;.get_month_link($year,$month).&#039;&quot;&gt;&#039;.$month_name.&#039;&lt;\/a&gt;\n\t&lt;\/h3&gt;&#039;;\n\t}else{\n\techo &#039;&lt;h2 class=&quot;month-title&quot;&gt;&#039;.$month_name.&#039;&lt;\/h2&gt;&#039;;\n\t}\n\n\n\/* ======================================\n   7. CALENDAR TABLE\n====================================== *\/\necho &#039;&lt;table class=&quot;calendar-month&quot;&gt;&#039;;\necho &#039;&lt;thead&gt;&lt;tr&gt;&#039;;\n$weekdays = &#x5B;&#039;M&#039;,&#039;T&#039;,&#039;W&#039;,&#039;T&#039;,&#039;F&#039;,&#039;S&#039;,&#039;S&#039;]; \/\/to change manually to your language\nforeach ($weekdays as $w){\n\techo &#039;&lt;th&gt;&#039;.$w.&#039;&lt;\/th&gt;&#039;;\n\t}\necho &#039;&lt;\/tr&gt;&lt;\/thead&gt;&#039;;\necho &#039;&lt;tbody&gt;&lt;tr&gt;&#039;;\n\n\/* first day of the month *\/\n$first_day = date(&#039;N&#039;, strtotime(&quot;$year-$month-01&quot;));\nfor ($i=1;$i&lt;$first_day;$i++){\n\techo &#039;&lt;td class=&quot;empty&quot;&gt;&lt;\/td&gt;&#039;;\n\t}\n$days = cal_days_in_month(CAL_GREGORIAN,$month,$year);\n$weekday = $first_day;\n\n\n\/* ======================================\n   8. CYCLE DAYS\n====================================== *\/\nfor ($day=1;$day&lt;=$days;$day++){\n\t$date = $year.&#039;-&#039;.sprintf(&#039;%02d&#039;,$month).&#039;-&#039;.sprintf(&#039;%02d&#039;,$day);\n\t$is_today_past = false;\n\t\/* \u2018today in the past years\u2019 comparison *\/\n\tif ($year &lt; $current_year &amp;&amp; $month==$today_month &amp;&amp; $day==$today_day){\n\t\t$is_today_past = true;\n\t}\n\t$classes = &amp;#91;&amp;#93;;\n\tif ($is_today_past) $classes&amp;#91;&amp;#93; = &#039;today-past&#039;;\n\tif (isset($post_dates&amp;#91;$date&amp;#93;)){\n\t\t$classes&amp;#91;&amp;#93; = &#039;has-posts&#039;;\n\t} else {\n\t\t$classes&amp;#91;&amp;#93; = &#039;no-posts&#039;;\n\t}\n\techo &#039;&lt;td class=&quot;&#039;.implode(&#039; &#039;, $classes).&#039;&quot;&gt;&#039;;\n\tif (isset($post_dates&#x5B;$date])){\n\t\t$count = $post_dates&#x5B;$date];\n\t\t$title = ($count==1) ? &#039;1 post&#039; : $count.&#039; posts&#039;;\n\t\techo &#039;&lt;a class=&quot;day-link&quot; href=&quot;&#039;.get_day_link($year,$month,$day).&#039;&quot; title=&quot;&#039;.$title.&#039;&quot;&gt;&#039;.$day.&#039;&lt;\/a&gt;&#039;;\n\t}else{\n\t\techo &#039;&lt;span class=&quot;day-number&quot;&gt;&#039;.$day.&#039;&lt;\/span&gt;&#039;;\n\t}\n\techo &#039;&lt;\/td&gt;&#039;;\n\tif ($weekday==7){\n\t\techo &#039;&lt;\/tr&gt;&lt;tr&gt;&#039;;\n\t\t$weekday=1;\n\t}else{\n\t\t$weekday++;\n\t}\n\t}\n\n\/* final table cells *\/\nwhile ($weekday&lt;=7){\n\techo &#039;&lt;td class=&quot;empty&quot;&gt;&lt;\/td&gt;&#039;;\n\t$weekday++;\n\t}\necho &#039;&lt;\/tr&gt;&lt;\/tbody&gt;&lt;\/table&gt;&#039;;\necho &#039;&lt;\/div&gt;&#039;;\n}\n\necho &#039;&lt;\/div&gt;&#039;;\n?&gt;\n<\/pre>\n<p>\nJust to&nbsp;be&nbsp;clear, here&rsquo;s something obvious: with the PHP code shown above, you need to&nbsp;create a&nbsp;template (a *.php file, for example calendarblog.php), upload it&nbsp;to&nbsp;your WordPress site&rsquo;s theme, and then use it&nbsp;to&nbsp;create the calendar page via the admin panel.<br \/>\nThe CSS code for displaying the calendar: it&nbsp;can be&nbsp;copied into the theme&rsquo;s style.css file or&nbsp;included separately:<\/p>\n<pre class=\"brush: css; collapse: false; title: listing 2; notranslate\" title=\"listing 2\">\n\/* ===== navigation by year ===== *\/\n.calendar-nav{\n\ttext-align:center;\n\tmargin:40px 0;\n\tfont-size:22px;\n\tline-height:2;\n}\n.calendar-nav a{\n\tmargin:0 10px;\n\ttext-decoration:none;\n}\n.current-year{\n\tfont-weight:bold;\n\tmargin:0 15px;\n}\n\/* ===== monthly layout ===== *\/\n.calendar-year{ display:block; }\n.calendar-month-block{ margin-bottom:40px; }\n\/* ===== month title ===== *\/\n.month-title{\n\ttext-align:center;\n\tmargin-bottom:10px;\n}\n\/* ===== table ===== *\/\n.calendar-month{\n\twidth:100%;\n\tborder-collapse:collapse;\n}\n.calendar-month th{\n\tpadding:5px;\n\ttext-align:center;\n\tfont-weight:bold;\n}\n.calendar-month td{\n\tpadding:0;\n\theight:32px;\n\ttext-align:center;\n}\n\/* ===== days ===== *\/\n.day-number{\n\tdisplay:block;\n\tpadding:6px;\n}\n\/* table cells with items *\/\n.calendar-month td.has-posts{ background:#C5C5C5; }\n\/* the link fills the entire cell *\/\n.day-link{\n\tdisplay:block;\n\twidth:100%;\n\theight:100%;\n\tpadding:6px;\n\ttext-decoration:none;\n\tfont-weight:bold;\n\tbackground:transparent; \/* VERY IMPORTANT *\/\n}\n\/* table cells without items *\/\n.calendar-month td.no-posts{ background:#FFFFFF; }\n\/* non-hyperlinked numbers *\/\n.day-number{\n\tdisplay:block;\n\tpadding:6px;\n\tbackground:#FFFFFF; \/* guarantees full white *\/\n}\n.today-past{ outline:2px dashed #FF9800; }\n\/* hover *\/\n.day-link:hover{ background:#C5C5C5; }\n\/* empty table cells *\/\n.empty{ background:#FFFFFF; }\n\/* ===== layout desktop ===== *\/\n@media (min-width:1024px){\n.calendar-year{\n\tdisplay:grid;\n\tgrid-template-columns:repeat(3,1fr);\n\tgap:30px;\n\talign-items:start;\n}\n.calendar-month-block{ margin-bottom:0; }\n}\n<\/pre>\n<p>\nIn&nbsp;this form, with these features, this is&nbsp;exactly the blog calendar I&nbsp;needed. What could or&nbsp;should be&nbsp;added? In&nbsp;fact, I&nbsp;am not entirely sure myself. If&nbsp;something comes to&nbsp;mind (or&nbsp;if&nbsp;someone gives me&nbsp;a&nbsp;suggestion), I&nbsp;will publish a&nbsp;second version and announce&nbsp;it.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>For quite some time, I&nbsp;had been meaning to&nbsp;create something like this on&nbsp;my&nbsp;website: an&nbsp;annual calendar (both for the current year and for each of&nbsp;the past years) in&nbsp;which, for every date, there is&nbsp;a&nbsp;link leading to&nbsp;a&nbsp;collection of&nbsp;blog posts published on&nbsp;that very day (if&nbsp;anything was actually published). On&nbsp;LiveJournal (and other blogging platforms), such a&nbsp;calendar existed and was very convenient <a href=\"https:\/\/eugigufo.net\/en\/blog\/982\/\">[&#8230;]<br \/>Read more >>><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5],"tags":[17,12],"class_list":["post-982","post","type-post","status-publish","format-standard","hentry","category-sitenews","tag-calendar","tag-wordpress"],"_links":{"self":[{"href":"https:\/\/eugigufo.net\/en\/wp-json\/wp\/v2\/posts\/982","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/eugigufo.net\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/eugigufo.net\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/eugigufo.net\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/eugigufo.net\/en\/wp-json\/wp\/v2\/comments?post=982"}],"version-history":[{"count":1,"href":"https:\/\/eugigufo.net\/en\/wp-json\/wp\/v2\/posts\/982\/revisions"}],"predecessor-version":[{"id":1018,"href":"https:\/\/eugigufo.net\/en\/wp-json\/wp\/v2\/posts\/982\/revisions\/1018"}],"wp:attachment":[{"href":"https:\/\/eugigufo.net\/en\/wp-json\/wp\/v2\/media?parent=982"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/eugigufo.net\/en\/wp-json\/wp\/v2\/categories?post=982"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/eugigufo.net\/en\/wp-json\/wp\/v2\/tags?post=982"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}