diff --git a/TUMBLR_MIGRATION_README.md b/TUMBLR_MIGRATION_README.md index 22565f7..ca9a917 100644 --- a/TUMBLR_MIGRATION_README.md +++ b/TUMBLR_MIGRATION_README.md @@ -288,3 +288,46 @@ The migration script creates 19 posts = 19 API calls, well within limits. **Last Updated:** January 31, 2026 **Maintainer:** OpenWorm Team + +--- + +## Posting an Event + +The website's [Events page](https://openworm.org/events.html) is drawn live from this +blog. There is nothing to edit on the website — post to Tumblr and the page updates. + +### Tags + +| Tag | Effect | +|-----|--------| +| `event` **or** `events` | Required. Puts the post on the Events page. Both spellings work; older posts use both. | +| `date:2026-09-15` | Optional. The date the event **happens**. | +| `date:2026-01-29..2026-01-30` | Optional. A multi-day event, rendered as "29-30 Jan". | + +A bare `2026-09-15` (no `date:` prefix) works too. + +### Why the date tag matters + +Without it the page falls back to the post's publication date — which is when the event +was *announced*, not when it happens. For anything upcoming those point in opposite +directions, and the event lands in the wrong section. + +A real example already on the blog: *"Join us in London! / November 5-6"* was posted on +**31 October 2014**, so it renders under 31 Oct. Tagging it `date:2014-11-05..2014-11-06` +would fix it. + +Malformed dates (`2026-13-99`, `2026-02-30`) are ignored and the post date is used +instead, so a typo degrades quietly rather than inventing a date. + +### Sections + +Events dated today or later appear under **Coming up**, soonest first. Everything else +appears under **Recently**, most recent first. Events predating the blog are kept as a +static **Earlier events** list in `events.html` — that list is hand-maintained and is +not affected by anything here. + +### Title and description + +The post title becomes the event title; the opening of the post body becomes the +description, trimmed to fit the card. Lead with the essentials — a post that opens with +"UPDATE 3 (2/11/17):" will show exactly that. diff --git a/events.html b/events.html index f7623d6..37583db 100644 --- a/events.html +++ b/events.html @@ -1,408 +1,226 @@ - - - OpenWorm Events - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
-
-

Events

- -

- Where we have been and where to meet us next! -

-
-
- -
-
-
-
-              -
- -
-
-
-
    - - -
  • - -
    -

    OpenWorm @ 2025 International Worm Meeting

    -

    The OpenWorm team were present at the International Worm Meeting at UC Davis in July 2025.

    - -
    -
  • - -
  • - -
    -

    OpenWorm @ C. elegans 2023 meeting

    -

    The OpenWorm team were present at the main C. elegans global scientific conference in Glasgow in June 2023.

    - -
    -
  • - -
  • - -
    -

    OpenWorm poster at C. elegans meeting

    -

    Online presentation of: The OpenWorm Project: progress update, available resources and future plans

    - -
    -
  • - -
  • - -
    -

    Open House @ Sainsbury Wellcome Centre

    -

    London, UK

    - -
    -
  • - - -
  • - -
    -

    Connectome to behaviour: modelling C. elegans at cellular resolution

    -

    Royal Society Discussion meeting, London, UK

    - -
    -
  • - -
  • - -
    -

    TEDxBangalore

    -

    Speaker: Stephen Larson, Bangalore, India

    - -
    -
  • - -
  • - -
    -

    Open Collaboration in Computational Neuroscience workshop

    -

    Neuroinformatics 2014, Leiden, The Netherlands

    - -
    -
  • - -
  • - -
    -

    Open Source Brain Workshop 2014

    -

    Alghero, Sardinia, Italy

    - -
    -
  • - -
  • - -
    -

    Neuroinformatics 2013

    -

    Karolinska Institutet, Stockholm, Sweden

    - -
    -
  • - -
  • - -
    -

    Computational Neuroscience Meeting 2013

    -

    Université Paris Descartes, Paris, France

    - -
    -
  • - -
  • - -
    -

    OpenWorm Paris Meet-up

    -

    Falstaff, Paris, France

    -
    -
  • - -
  • - -
    -

    Guest Lecture: Introduction to OpenWorm

    -

    Speaker: Matteo Cantarelli. Host: Daniele Giusto - Faculty of Electrical Engineering, University Of Cagliari, Italy

    -
    -
  • - -
  • - -
    -

    Open Source Brain Kick-Off meeting

    -

    Alghero, Sardinia, Italy

    - -
    -
  • - -
  • - -
    -

    OpenWorm Cambridge Meet-up

    -

    The Eagle, Cambridge, UK

    -
    -
  • - -
  • - -
    -

    OpenWorm London Meet-up

    -

    Queens Head & Artichoke, London, UK

    -
    -
  • - -
  • - -
    -

    Modeling C. elegans: The OpenWorm Project

    -

    Speaker: Mike Vella, Cambridge, UK

    - -
    -
  • - -
  • - -
    -

    NeuroInformatics 2012

    -

    Boston, USA

    - -
    -
  • - -
  • - -
    -

    Convergence in Computational Neuroscience 2012

    -

    Informatics Forum, Edinburgh, UK

    - -
    -
  • - -
  • - -
    -

    NeuroInformatics 2011

    -

    Informatics Forum, Edinburgh, UK

    - -
    -
  • - -
  • - -
    -

    NeuroML Development Workshop

    -

    Goodenough College, London, UK

    - -
    -
  • - - - -
-
-
-
- -
-
- - -
- - - - -
- - - - - - - - - - - - - - - + + + OpenWorm Events + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+

OpenWorm Events

+

+ Where we have been and where to meet us next +

+

+ See events on Tumblr » +

+
+
+ +
+ +
+ +
+ +
+ + +
+ + + +
    +
  • + +

    Loading events from the blog...

    +
  • +
+ + + +
+
+ +
+ + +
+ + +
+ + + + + + + + diff --git a/js/main.js b/js/main.js index 13c579a..9956176 100644 --- a/js/main.js +++ b/js/main.js @@ -21,6 +21,8 @@ $(document).on('pjax:complete', function() { loadDonationControls(); } else if (loc === '/news.html') { loadFullNewsFeed(); + } else if (loc === '/events.html') { + loadEventsFeed(); } else if (loc === '/people.html') { $.getScript("https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.2.0/mustache.min.js", function() { @@ -93,6 +95,8 @@ $(window).on('load', function() { loadDonationControls(); } else if (loc === '/news.html') { loadFullNewsFeed(); + } else if (loc === '/events.html') { + loadEventsFeed(); } else if (loc === '/people.html') { $.getScript("https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.2.0/mustache.min.js", function() { @@ -131,148 +135,403 @@ function setNavigation() { } -function getTumblrPostTitle(post) { - if (post['regular-title']) return post['regular-title']; - if (post['link-text']) return post['link-text']; - if (post['quote-text']) return post['quote-text'].substring(0, 100); - if (post['photo-caption']) { - var tmp = document.createElement('div'); - tmp.innerHTML = post['photo-caption']; - return (tmp.textContent || tmp.innerText || '').substring(0, 100); +// ============================================================ +// Tumblr feed client - shared by index.html, news.html, events.html +// ============================================================ +// +// Tumblr put its legacy /api/read/json endpoint behind a bot challenge; it +// now answers 403 for ordinary visitors, which is what silently broke the +// news feed. Everything below uses the supported v2 API instead. +// +// TUMBLR_API_KEY is a public, read-only consumer key. It can only read posts +// that are already public on the blog - it cannot post, edit, or delete. + +var TUMBLR_BLOG = 'openworm.tumblr.com'; +var TUMBLR_API_KEY = 'Sr3W9pREVcJrnwiZb57tj8wRuCbP9d1npEynWqotXMoN5DDj9P'; +var TUMBLR_CACHE_MIN = 15; + +// Posts tagged with any of these show up on the events page. Both spellings +// are in use on the blog already, so accept both rather than silently +// dropping half the history. +var TUMBLR_EVENT_TAGS = ['event', 'events']; + +var _tumblrPending = {}; + +function _tumblrCacheGet(key) { + try { + var raw = window.sessionStorage.getItem(key); + if (!raw) return null; + var hit = JSON.parse(raw); + if ((Date.now() - hit.at) > TUMBLR_CACHE_MIN * 60 * 1000) return null; + return hit.posts; + } catch (e) { return null; } +} + +function _tumblrCacheSet(key, posts) { + try { + window.sessionStorage.setItem(key, JSON.stringify({ at: Date.now(), posts: posts })); + } catch (e) { /* private browsing / quota - caching is optional */ } +} + +// Fetch posts. opts: {tag, limit, done, fail} +// +// At most ONE request per distinct query is in flight at a time. Duplicate +// callers (window load racing pjax:complete, say) attach to the request that +// is already running instead of firing their own and fighting over the DOM. +function tumblrPosts(opts) { + opts = opts || {}; + var tag = opts.tag || ''; + var limit = opts.limit || 20; + var key = 'tumblr:' + tag + ':' + limit; + + var cached = _tumblrCacheGet(key); + if (cached) { if (opts.done) opts.done(cached); return; } + + if (_tumblrPending[key]) { _tumblrPending[key].push(opts); return; } + _tumblrPending[key] = [opts]; + + var settle = function(which, arg) { + var waiting = _tumblrPending[key] || []; + delete _tumblrPending[key]; + for (var i = 0; i < waiting.length; i++) { + if (waiting[i][which]) waiting[i][which](arg); + } + }; + + var params = { api_key: TUMBLR_API_KEY, limit: limit, filter: 'html' }; + if (tag) params.tag = tag; + + $.ajax({ + url: 'https://api.tumblr.com/v2/blog/' + TUMBLR_BLOG + '/posts', + data: params, + dataType: 'jsonp', + timeout: 15000 + }).done(function(data) { + var posts = (data && data.response && data.response.posts) || []; + _tumblrCacheSet(key, posts); + settle('done', posts); + }).fail(function(xhr, status) { + console.error('Tumblr fetch failed [' + key + ']:', status); + settle('fail', status); + }); +} + +// Fetch several tags at once and merge, newest first, de-duplicated by id. +function tumblrPostsForTags(tags, limit, done, fail) { + var collected = [], seen = {}, remaining = tags.length, anyOk = false; + + var finish = function() { + if (--remaining > 0) return; + if (!anyOk) { if (fail) fail('all tag queries failed'); return; } + collected.sort(function(a, b) { return b.timestamp - a.timestamp; }); + done(collected); + }; + + for (var i = 0; i < tags.length; i++) { + tumblrPosts({ + tag: tags[i], + limit: limit, + done: function(posts) { + anyOk = true; + for (var j = 0; j < posts.length; j++) { + if (!seen[posts[j].id]) { seen[posts[j].id] = true; collected.push(posts[j]); } + } + finish(); + }, + fail: finish + }); } - if (post['regular-body']) { - var tmp = document.createElement('div'); - tmp.innerHTML = post['regular-body']; - var text = tmp.textContent || tmp.innerText || ''; - return text.indexOf(':') !== -1 ? text.substring(0, text.indexOf(':')) : text; +} + +// ---- post field helpers (v2 shapes) ------------------------------------ + +function tumblrStripHtml(html) { + var tmp = document.createElement('div'); + tmp.innerHTML = html || ''; + return (tmp.textContent || tmp.innerText || '').replace(/\s+/g, ' ').trim(); +} + +// Tumblr returns post titles as plain text, so the italics that the rest of +// the site applies to species names are lost. Restore them at render time - +// the repo has explicit commits ("Italicise C. elegans") establishing this as +// house style, and the hand-written archive entries already follow it. +function italiciseSpecies(s) { + if (!s || s.indexOf('') !== -1 || s.indexOf('') !== -1) return s; + return s.replace(/\b(C\. elegans|Caenorhabditis elegans|Drosophila)\b/g, '$1'); +} + +// Photo and quote posts have no title, so one has to be derived from the +// caption. Cut at the first real sentence end rather than at a fixed width, +// otherwise the "title" is a mid-sentence slice of the body and the two read +// as duplicates. Abbreviations matter here: a naive split on ". " would cut +// "C. elegans" in half on a C. elegans blog. +function tumblrDeriveTitle(text) { + var line = (text || '').split('\n')[0].trim(); + if (line.length <= 60) return line; + var m = /[^\s]{2,}[.!?](\s|$)/.exec(line); + if (m && m.index + m[0].length >= 20 && m.index + m[0].length <= 130) { + return line.substring(0, m.index + m[0].trimEnd().length); + } + return line.substring(0, 120); +} + +function tumblrPostTitle(post) { + if (post.title) return post.title; + if (post.summary) return tumblrDeriveTitle(post.summary); + var text = tumblrStripHtml(post.body || post.caption || post.description || post.text); + if (!text) return 'Untitled post'; + return text.indexOf(':') !== -1 && text.indexOf(':') < 80 + ? text.substring(0, text.indexOf(':')) + : text.substring(0, 120); +} + +function tumblrPostBody(post) { + var html = ''; + if (post.type === 'photo' && post.photos && post.photos.length) { + var src = post.photos[0].original_size && post.photos[0].original_size.url; + if (src) html += ''; } - return 'Untitled post'; + html += post.body || post.caption || post.description || ''; + if (!html && post.text) { + html = '
' + post.text + '
' + (post.source || ''); + } + return html; } +// NPF posts carry their title as a heading block inside the body, so a naive +// render prints the title twice - once as the item heading, once at the top of +// the text. Drop the leading copy when it duplicates the heading we already +// rendered. +function stripLeadingTitle(html, title) { + if (!html || !title) return html; + var plain = tumblrStripHtml(title); + if (!plain) return html; + + var el = document.createElement('div'); + el.innerHTML = html; + + // Compare decoded text, not raw HTML: a title containing "&" appears as + // "&" in the markup, so string matching on the source silently fails. + // Skip leading media (photo posts open with an ) to reach the caption. + for (var i = 0; i < el.children.length; i++) { + var node = el.children[i]; + var txt = tumblrStripHtml(node.innerHTML); + if (!txt) continue; // img, spacer, empty node + if (txt === plain) { + node.parentNode.removeChild(node); + return el.innerHTML; + } + if (txt.indexOf(plain) === 0) { // title runs on into the body + var rest = txt.slice(plain.length).replace(/^[\s.,:;!?-]+/, ''); + node.textContent = rest; + return el.innerHTML; + } + break; // first real text isn't the title + } + return el.innerHTML; +} + +function tumblrFormatDate(d) { + return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); +} + +// ---- event date convention --------------------------------------------- +// +// A post's publication date is when the event was ANNOUNCED, which is not +// the same thing as when the event happens - and for anything upcoming they +// point in opposite directions. So an event post may carry a date tag: +// +// date:2026-09-15 single day +// date:2026-01-29..2026-01-30 a range, rendered "29-30" +// +// With no date tag we fall back to the post's own date, which is the right +// answer for the historical posts that predate this convention. +function tumblrEventDate(post) { + // Date.UTC silently rolls impossible values over - Date.UTC(2026, 12, 99) + // is April 2027, not an error - so a typo in a tag would quietly produce a + // confidently wrong date. Build the date, then check it reads back the way + // it was written; anything that does not is treated as not a date tag. + var toUTC = function(y, mo, d) { + var dt = new Date(Date.UTC(y, mo - 1, d)); + if (dt.getUTCFullYear() !== y || dt.getUTCMonth() !== mo - 1 || dt.getUTCDate() !== d) return null; + return dt; + }; + + var tags = post.tags || []; + var re = /^(?:date:)?(\d{4})-(\d{2})-(\d{2})(?:\.\.(\d{4})-(\d{2})-(\d{2}))?$/; + for (var i = 0; i < tags.length; i++) { + var m = re.exec($.trim(tags[i])); + if (!m) continue; + var start = toUTC(+m[1], +m[2], +m[3]); + if (!start) continue; + var end = start; + if (m[4]) { + end = toUTC(+m[4], +m[5], +m[6]); + if (!end || end < start) end = start; + } + return { start: start, end: end, explicit: true }; + } + var posted = new Date(post.timestamp * 1000); + return { start: posted, end: posted, explicit: false }; +} + +// ---- home page: short news list ---------------------------------------- + function refreshNews() { - // Use Tumblr v1 JSONP API (no CORS needed) - $.ajax({ - url: 'https://openworm.tumblr.com/api/read/json', - data: { num: 6 }, - dataType: 'jsonp', - timeout: 30000, - success: function(data) { - var posts = data.posts || []; + tumblrPosts({ + limit: 6, + done: function(posts) { var html = ''; - for (var i = 0; i < posts.length; i++) { var post = posts[i]; - var title = getTumblrPostTitle(post); - var link = post['url-with-slug'] || post['url']; - var pubDate = new Date(post['unix-timestamp'] * 1000); - var dateStr = pubDate.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric' - }); - html += '
  • '; - html += '' + title + ''; - html += ' (' + dateStr + ')'; + html += '' + italiciseSpecies(tumblrPostTitle(post)) + ''; + html += ' (' + tumblrFormatDate(new Date(post.timestamp * 1000)) + ')'; html += '
  • '; } - $("#news-feed").html(html); + $('#news-feed').html(html); }, - error: function(err) { - console.error('Error loading news feed:', err); - $("#news-feed").html('
  • Unable to load news feed.
  • '); + fail: function() { + $('#news-feed').html('
  • Unable to load the news feed right now. ' + + 'Read it on Tumblr »
  • '); } }); } -function getTumblrPostBody(post) { - if (post['regular-body']) return post['regular-body']; - if (post['photo-caption']) return post['photo-caption']; - if (post['link-description']) return post['link-description']; - if (post['quote-text']) return '
    ' + post['quote-text'] + '
    ' + (post['quote-source'] || ''); - return ''; -} +// ---- news.html: full feed with sidebar --------------------------------- function loadFullNewsFeed() { - // Load full news feed with sidebar navigation for news.html page - console.log('Loading full news feed with sidebar...'); - - $.ajax({ - url: 'https://openworm.tumblr.com/api/read/json', - data: { num: 25 }, - dataType: 'jsonp', - timeout: 30000, - success: function(data) { - console.log('Feed loaded via JSONP'); - - var posts = data.posts || []; - console.log('Found ' + posts.length + ' posts'); - + tumblrPosts({ + limit: 25, + done: function(posts) { var mainHtml = ''; - var navHtml = ''; + var navHtml = ''; for (var i = 0; i < posts.length; i++) { - var post = posts[i]; - var title = getTumblrPostTitle(post); - var link = post['url-with-slug'] || post['url']; - var pubDate = new Date(post['unix-timestamp'] * 1000); - var dateStr = pubDate.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric' - }); - - var description = getTumblrPostBody(post); - - // Create anchor ID from index - var anchorId = 'news-' + i; - - // Add to sidebar nav (if nav element exists) - navHtml += '
  • ' + dateStr + '
  • '; - - // Add to main content - var borderStyle = (i < posts.length - 1) ? 'border-bottom: 1px solid #eee;' : ''; - mainHtml += '
  • '; - mainHtml += '

    ' + title + '

    '; + var post = posts[i]; + var dateStr = tumblrFormatDate(new Date(post.timestamp * 1000)); + var anchor = 'news-' + i; + + navHtml += '
  • ' + dateStr + '
  • '; + + var border = (i < posts.length - 1) ? 'border-bottom: 1px solid #eee;' : ''; + mainHtml += '
  • '; + mainHtml += '

    ' + italiciseSpecies(tumblrPostTitle(post)) + '

    '; mainHtml += '

    ' + dateStr + '

    '; - mainHtml += '
    ' + description + '
    '; + mainHtml += '
    ' + stripLeadingTitle(tumblrPostBody(post), tumblrPostTitle(post)) + '
    '; mainHtml += '
  • '; } - $("#news-feed-full").html(mainHtml); - - // Update sidebar nav if it exists - if ($("#news-nav").length) { - $("#news-nav").html(navHtml); - } - - console.log('Rendered ' + posts.length + ' items'); - - // Make images responsive - $("#news-feed-full img").css({ - "max-width": "100%", - "height": "auto", - "margin": "15px 0", - "display": "block" + $('#news-feed-full').html(mainHtml); + if ($('#news-nav').length) $('#news-nav').html(navHtml); + + $('#news-feed-full img').css({ + 'max-width': '100%', 'height': 'auto', 'margin': '15px 0', 'display': 'block' }); }, - error: function(xhr, status, err) { - console.error('Error loading full feed - Status:', status, 'Error:', err); - - var errorMsg = 'Unable to load news feed. '; - if (status === 'timeout') { - errorMsg += 'Request timed out.'; - } else { - errorMsg += 'Error: ' + status; + fail: function(status) { + var msg = (status === 'timeout') ? 'The request timed out.' : 'The feed could not be reached.'; + $('#news-feed-full').html('
  • ' + + msg + ' Read the blog directly »
  • '); + if ($('#news-nav').length) { + $('#news-nav').html('' + + '
  • View on Tumblr
  • '); } + } + }); +} + +// ---- events.html: events drawn from tagged blog posts ------------------- + +var MONTH_ABBR = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + +// "Jun 22, 2021" for a single day, "Jan 29-30, 2018" for a range - matching +// how the hand-written archive entries below the feed are written. +function tumblrFormatEventDate(when) { + var s = when.start, e = when.end; + var out = MONTH_ABBR[s.getUTCMonth()] + ' ' + s.getUTCDate(); + if (e.getTime() !== s.getTime()) { + out += (e.getUTCMonth() === s.getUTCMonth()) + ? '\u2013' + e.getUTCDate() + : '\u2013' + MONTH_ABBR[e.getUTCMonth()] + ' ' + e.getUTCDate(); + } + return out + ', ' + s.getUTCFullYear(); +} + +// Render one post in the same shape as a news item and as the archived +// events below it, so the whole page reads as one list. +function renderEventItem(post, anchor, isLast) { + var dateStr = tumblrFormatEventDate(tumblrEventDate(post)); + var border = isLast ? '' : 'border-bottom: 1px solid #eee;'; + + var html = '
  • '; + html += '

    ' + italiciseSpecies(tumblrPostTitle(post)) + '

    '; + html += '

    ' + dateStr + '

    '; - $("#news-feed-full").html('
  • ' + errorMsg + ' View blog directly »
  • '); - - // Update sidebar nav with fallback if it exists - if ($("#news-nav").length) { - $("#news-nav").html('
  • View on Tumblr
  • '); + var body = tumblrStripHtml(stripLeadingTitle(tumblrPostBody(post), tumblrPostTitle(post))); + if (body.length > 320) body = body.substring(0, 317).replace(/\s+\S*$/, '') + '...'; + if (body) html += '
    ' + body + '
    '; + + html += '

    ' + + ' Read the post

    '; + html += ''; + return html; +} + +function _eventSectionHeading(text) { + return '
  • ' + text + '

  • '; +} + +function loadEventsFeed() { + var $feed = $('#events-feed'); + if (!$feed.length) return; + + tumblrPostsForTags(TUMBLR_EVENT_TAGS, 50, function(posts) { + // "Today" at UTC midnight, so an event happening today still counts + // as upcoming rather than dropping into the past the morning of. + var now = new Date(); + var today = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()); + + var upcoming = [], past = []; + for (var i = 0; i < posts.length; i++) { + (tumblrEventDate(posts[i]).end.getTime() >= today ? upcoming : past).push(posts[i]); + } + // Soonest first for what is ahead; most recent first for what is behind. + upcoming.sort(function(a, b) { return tumblrEventDate(a).start - tumblrEventDate(b).start; }); + past.sort(function(a, b) { return tumblrEventDate(b).start - tumblrEventDate(a).start; }); + + var html = '', navHtml = '', n = 0; + var addGroup = function(label, list) { + if (!list.length) return; + html += _eventSectionHeading(label); + for (var j = 0; j < list.length; j++) { + var anchor = 'event-' + (n++); + var last = (list === past) && (j === list.length - 1); + html += renderEventItem(list[j], anchor, last); + navHtml += '
  • ' + + tumblrFormatEventDate(tumblrEventDate(list[j])) + '
  • '; } + }; + addGroup('Coming up', upcoming); + addGroup('Recently', past); + + if (!html) { + html = '
  • No events posted to the blog yet — earlier events are listed below.
  • '; } + $feed.html(html); + $('#events-nav-loading').replaceWith(navHtml || + '
  • ' + + ' On Tumblr
  • '); + }, function() { + $feed.html('
  • ' + + 'Could not reach the blog just now. ' + + 'See events on Tumblr »
    ' + + 'Earlier events are listed below.
  • '); + $('#events-nav-loading').replaceWith( + '
  • ' + + ' On Tumblr
  • '); }); } diff --git a/news.html b/news.html index 20f67ca..3758dd7 100644 --- a/news.html +++ b/news.html @@ -101,13 +101,9 @@

    Latest News

    - +