more blog posts
235
blog/a-decade-in-the-industry-a-ten-year-retrospective.html
Normal file
|
@ -0,0 +1,235 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>A decade in the industry; a ten year retrospective - NodeBB - Modern Community Forum Software</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Blog of NodeBB Forum Software - The Modern Discussion Platform">
|
||||
<meta name="author" content="NodeBB Inc.">
|
||||
<meta name="keywords" content="nodebb, node.js, forum, discussion, community, software, hosting, blog">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/icons/32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/icons/16x16.png">
|
||||
<link rel="shortcut icon" href="/images/icons/favicon.ico">
|
||||
|
||||
<!-- Google Fonts: Inter & Poppins -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Poppins:wght@400;600&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<!-- our css-->
|
||||
<link href="/css/style.css" rel="stylesheet">
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-V0P62EB8Q6"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-V0P62EB8Q6');
|
||||
</script>
|
||||
</head>
|
||||
<body class="p-0 py-5 p-lg-5">
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar navbar-expand-lg bg-body fixed-top shadow-sm">
|
||||
<div class="container-lg">
|
||||
<a class="navbar-brand py-2" href="/">
|
||||
<img src="/images/brand/nodebb-logo.svg" style="height: 36px; width: auto;" alt="NodeBB Logo" />
|
||||
</a>
|
||||
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMenu" aria-controls="navbarMenu" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse justify-content-end" id="navbarMenu">
|
||||
<ul class="nav nav-underline gap-4 flex-column flex-lg-row align-items-end align-items-lg-center mt-4 mt-lg-0">
|
||||
<li class="nav-item">
|
||||
<a href="/" class="nav-link text-reset fw-semibold">HOME</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/product" class="nav-link text-reset fw-semibold">PRODUCT</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/pricing" class="nav-link text-reset fw-semibold">PRICING</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/services" class="nav-link text-reset fw-semibold">SERVICES</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" target="_blank" role="button" data-bs-toggle="dropdown" aria-expanded="false" class="nav-link dropdown-toggle text-reset fw-semibold">RESOURCES</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end border-0 shadow p-1">
|
||||
<li><a href="https://community.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Community</a></li>
|
||||
<li><a href="https://try.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Demo Site</a></li>
|
||||
<li><a href="https://community.nodebb.org/category/28/answers" class="dropdown-item rounded-1" target="_blank">Answers</a></li>
|
||||
<li><a href="https://docs.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Documentation</a></li>
|
||||
<li><a href="/bounty" class="dropdown-item rounded-1">Bug Bounty</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/contact" class="btn btn-warning rounded-pill">Contact Us</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="https://manage.nodebb.org/register" class="btn btn-primary"><i class="fa-solid fa-rocket"></i> Start Free Trial</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="container-lg mt-5">
|
||||
<!-- Home -->
|
||||
<div id="home-tab-pane">
|
||||
<div class="row pt-2 pt-lg-5">
|
||||
<div class="col-12 d-flex flex-column gap-4">
|
||||
<h1 class="display-1 fw-bold fs-1">
|
||||
A decade in the industry; a ten year retrospective
|
||||
</h1>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- blog post -->
|
||||
<div class="py-5">
|
||||
<div class="d-flex gap-2 align-items-center mb-4">
|
||||
<a href="https://community.nodebb.org/user/julian"><img class="rounded-circle" width="32" src="https://community.nodebb.org/assets/uploads/profile/uid-2/2-profileavatar-1738544541106.jpeg"></a> <a href="https://community.nodebb.org/user/julian" class="fw-semibold">Julian Lam</a> <span class="text-secondary">8/15/2020, 5:39:07 PM</span>
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="kg-card-markdown">
|
||||
<p>Recently, a milestone had passed me by without my knowing – the original start date for my very first programming job was May 3<sup>rd</sup>, 2010. Ten years have flown by (or in some cases, have dragged by), and much has changed both in my personal and professional life, and much has stayed the same. I thought it would be a nice exercise to look back and see where I came from.</p>
|
||||
|
||||
<p>That first job was for a PHP Intern position at a now defunct company called <a href="https://www.crunchbase.com/organization/social-game-universe">Social Game Universe</a>. By day, this company made Facebook games, and did consulting for clients looking to get their games made, but SGU (as we called it then) also developed a platform for games to hook into, allowing for actions and interactions <em>between</em> apps/games, which was a novel idea at the time.</p>
|
||||
|
||||
<p>At the time, I believed that we were first-to-market, but we ended up getting beat out by <a href="https://parseplatform.org/">Parse</a>, which completely upended the industry and once Facebook bought out parse, it spelled the end of that particular project's potential.</p>
|
||||
|
||||
<p>However, <strong>more importantly</strong>, SGU was where I first met <a href="https://blog.nodebb.org/meet-the-leadership-team/">Andrew and Baris</a>. Andrew had joined the team around the same time, and Baris, then new to Canada, had actually joined Bitcasters, another company working in the same office, before it was later folded into Social Game Universe.</p>
|
||||
|
||||
<p>I will always look back fondly on this first job, as it provided me with a safe a welcoming space (when it wasn't crunch time, of course) to learn javascript and experiment with new ideas. Many of the lessons I learned at SGU I later put into practice at my subsequent jobs, including at NodeBB.</p>
|
||||
|
||||
<h2 id="stateoftheindustry">State of the industry</h2>
|
||||
<p>2010 was only a couple years past the DHTML craze of the early aughts. Javascript was very much still considered a "toy" language, but developers were starting to embrace it for website enhancements (think XHR/AJAX for dynamically loading content, etc.)</p>
|
||||
|
||||
<p>Some notable trends – YUI and Dojo were going strong, but faltering to newer entrants like Mootools and jQuery (spoiler alert, only one of those names is recognized now...).</p>
|
||||
|
||||
<p>Mootools ended up losing to jQuery, but will always be remembered as the reason why we don't have <code>Array.contains()</code>, but instead use <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes"><code>Array.includes()</code></a>. Mootools had no qualms about extending the prototype, and was popular enough at the time that its <code>Array.contains()</code> method would conflict badly, if one were provided by the browser, and so <code>Array.includes()</code> it was... after all, one doesn't simply break the web!</p>
|
||||
|
||||
<p>I remember betting on Mootools, but just like HD-DVD and Blu-ray, eventually the other side won out ?</p>
|
||||
|
||||
<p>While I was at that first job, I also had the bright idea to load page html via AJAX, isntead of doing a whole page load, thereby saving on loading all of the page boilerplate. Little did I know, I re-invented the concept of the single-page application. <a href="https://en.wikipedia.org/wiki/Convergent_evolution">Talk about convergent evolution!</a></p>
|
||||
|
||||
<h3 id="ohdidimentionthatthisjobwasunpaid"><strong>Oh, did I mention that this job was UNPAID?!</strong></h3>
|
||||
<p>At the time, the gaming industry was exploding. Games like Farmville and Words with Friends were capturing huge audiences, and many like myself looked at these smaller gaming studios as a way into the industry, hopefully landing at one of the triple A game studios later on.</p>
|
||||
|
||||
<p>Everybody wanted in, and so conditions were ripe for exploiting programmers for low or no pay!</p>
|
||||
|
||||
<h2 id="angryyoungmen">Angry young men...</h2>
|
||||
<p>Back then, we were naive, with ambitious ideas that we wanted to execute ASAP. Looking back, stifling this behaviour would have been the worst thing to do. Nurturing and exploring this aspect of our youth would be the best gift you could give to someone learning. We often poke fun at this internally, with the phrase "angry young men", functioning to remind us of how we were often quick to anger and always thought we knew what the best practice ought to be.</p>
|
||||
|
||||
<p>Over the years, I personally have learned a lot, including (but of course, not limited to):</p>
|
||||
<ul>
|
||||
<li>Leadership and management</li>
|
||||
<li>Business development</li>
|
||||
<li>Best practices for programming (duh)</li>
|
||||
<li>Soft skills such as conflict resolution</li>
|
||||
<li>Organisational skills, and project coordination</li>
|
||||
</ul>
|
||||
<p>There is much more for me to learn, and I can't wait to see how I will continue to grow as a developer in the next 10 years.</p>
|
||||
|
||||
<h2 id="wheredidyouseeyourselfbeingin10yearstime">Where did you see yourself being in 10 years' time?</h2>
|
||||
<p>Mark Zuckerberg once opined that <a href="https://www.cnet.com/news/say-what-young-people-are-just-smarter/">"young people are just smarter"</a>. While in many ways that can be true, I can say without hesitation that I am a better programmer than I was 10 years ago.</p>
|
||||
|
||||
<p>My one takeaway would be:</p>
|
||||
<blockquote>If you look back at your old code and are proud of it, then that means <strong>you have not grown as a developer</strong>.</blockquote>
|
||||
<p>There is merit in youthful talent, and nobody works harder than someone with something to prove, but age and experience teaches you to work smarter, not harder.</p>
|
||||
|
||||
<p>Nothing put this into sharp relief quite like having a baby, combined with the coronavirus pandemic. My son, Zachary, was born September 2019, and with it disappeared much of my free time that I had taken for granted. I finally understood what it meant to "make time" for something, because there was literally no more time in the day that wasn't better spent doing something else. "Down time" no longer existed. The coronavirus pandemic later threw the world for a loop in terms of working from home/adjusted work hours, which led to another revelation: while I was getting fewer hours in at my desk, I was working much more efficiently during those hours, and I would spend less time banging my head against non-working code, simply because I couldn't afford to do so (if I have no childcare, my day is essentially broken up into chunks that correspond with his naps.)</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<p>10 years ago, if you had asked me where I would see myself today, I probably would have answered with a variation of the following:</p>
|
||||
<ul>
|
||||
<li>Working at a large, pubicly traded corporation</li>
|
||||
<li>Job safety was my main concern at that time, I firmly believed in a steady paycheque</li>
|
||||
<li>Perhaps moving into management, despite my passion for solving problems and creating interesting projects</li>
|
||||
<li>Ideally in a government job, working for the city, etc.</li>
|
||||
</ul>
|
||||
|
||||
<p>I would never have imagined that I would've made the jump to entrepreneurship, the antithesis of a steady paycheque! I'll always be grateful to Andrew and Baris for pushing me towards starting up our own company (Design Create Play in 2013, and NodeBB in 2014). The rest, as they say, is history.</p>
|
||||
|
||||
<h2 id="whatsoursecret">What's our secret?</h2>
|
||||
|
||||
<p>Slow and steady wins the race. We didn't want to be a flash in the pan, in those early days, profitability was our main goal.</p>
|
||||
|
||||
<p>Chasing the hockey stick is sexy, but is by no means a guarantee.</p>
|
||||
|
||||
<p>Take time often to stop, re-evaluate, reposition, and continue upwards.</p>
|
||||
|
||||
<p>Here's to a decade in the industry, and here's to 10 more.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-5"/>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<div class="d-flex flex-column flex-md-row gap-5 justify-content-between mb-5 flex-wrap">
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Get Started</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/product" class="link-secondary text-decoration-none">PRODUCT</a></li>
|
||||
<li><a href="/pricing" class="link-secondary text-decoration-none">PRICING</a></li>
|
||||
<li><a href="/services" class="link-secondary text-decoration-none">SERVICES</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Resources</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="https://community.nodebb.org/" class="link-secondary text-decoration-none" target="_blank">COMMUNITY</a></li>
|
||||
<li><a href="https://try.nodebb.org/" class="link-secondary text-decoration-none" target="_blank">DEMO SITE</a></li>
|
||||
<li><a href="https://community.nodebb.org/category/28/answers" class="link-secondary text-decoration-none" target="_blank">ANSWERS</a></li>
|
||||
<li><a href="https://docs.nodebb.org" class="link-secondary text-decoration-none" target="_blank">DOCUMENTATION</a></li>
|
||||
<li><a href="/bounty" class="link-secondary text-decoration-none">BUG BOUNTY</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Company</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/about" class="link-secondary text-decoration-none">ABOUT</a></li>
|
||||
<li><a href="/blog" class="link-secondary text-decoration-none">BLOG</a></li>
|
||||
<li><a href="/contact" class="link-secondary text-decoration-none">CONTACT</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<a href="https://manage.nodebb.org/register" class="btn btn-primary"><i class="fa-solid fa-rocket"></i> Start Free Trial</a>
|
||||
<div class="d-flex gap-3 mt-3 justify-content-between px-2">
|
||||
<a title="NodeBB Github Page" href="https://github.com/nodebb/nodebb" class="link-secondary text-decoration-none"><i class="fab fa-github"></i></a>
|
||||
<a title="NodeBB Twitter Page" href="https://twitter.com/nodebb" class="link-secondary text-decoration-none"><i class="fab fa-twitter"></i></a>
|
||||
<a title="NodeBB Mastodon Page" href="https://fosstodon.org/@nodebb" class="link-secondary text-decoration-none"><i class="fa-brands fa-mastodon"></i></a>
|
||||
<a title="NodeBB Facebook Page" href="https://www.facebook.com/NodeBB" class="link-secondary text-decoration-none"><i class="fab fa-facebook"></i></a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap justify-content-between gap-5 small">
|
||||
<span class="text-secondary text-nowrap">©2025 NodeBB, Inc. — Made in Canada with <i class="fa-solid fa-heart text-danger"></i>.</span>
|
||||
<div class="d-flex gap-3">
|
||||
<a href="/tos" class="link-secondary text-nowrap text-decoration-none">Terms of Service</a>
|
||||
<a href="/privacy" class="link-secondary text-nowrap text-decoration-none">Privacy Policy</a>
|
||||
<a href="/gdpr" class="link-secondary text-nowrap text-decoration-none">GDPR</a>
|
||||
<a href="/dmca" class="link-secondary text-nowrap text-decoration-none">DMCA</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap 5 JS Bundle (includes Popper) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
181
blog/digitally-overwhelmed-podcast-episode-140.html
Normal file
|
@ -0,0 +1,181 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Digitally Overwhelmed Podcast, Episode 140 - NodeBB - Modern Community Forum Software</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Blog of NodeBB Forum Software - The Modern Discussion Platform">
|
||||
<meta name="author" content="NodeBB Inc.">
|
||||
<meta name="keywords" content="nodebb, node.js, forum, discussion, community, software, hosting, blog">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/icons/32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/icons/16x16.png">
|
||||
<link rel="shortcut icon" href="/images/icons/favicon.ico">
|
||||
|
||||
<!-- Google Fonts: Inter & Poppins -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Poppins:wght@400;600&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<!-- our css-->
|
||||
<link href="/css/style.css" rel="stylesheet">
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-V0P62EB8Q6"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-V0P62EB8Q6');
|
||||
</script>
|
||||
</head>
|
||||
<body class="p-0 py-5 p-lg-5">
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar navbar-expand-lg bg-body fixed-top shadow-sm">
|
||||
<div class="container-lg">
|
||||
<a class="navbar-brand py-2" href="/">
|
||||
<img src="/images/brand/nodebb-logo.svg" style="height: 36px; width: auto;" alt="NodeBB Logo" />
|
||||
</a>
|
||||
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMenu" aria-controls="navbarMenu" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse justify-content-end" id="navbarMenu">
|
||||
<ul class="nav nav-underline gap-4 flex-column flex-lg-row align-items-end align-items-lg-center mt-4 mt-lg-0">
|
||||
<li class="nav-item">
|
||||
<a href="/" class="nav-link text-reset fw-semibold">HOME</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/product" class="nav-link text-reset fw-semibold">PRODUCT</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/pricing" class="nav-link text-reset fw-semibold">PRICING</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/services" class="nav-link text-reset fw-semibold">SERVICES</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" target="_blank" role="button" data-bs-toggle="dropdown" aria-expanded="false" class="nav-link dropdown-toggle text-reset fw-semibold">RESOURCES</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end border-0 shadow p-1">
|
||||
<li><a href="https://community.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Community</a></li>
|
||||
<li><a href="https://try.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Demo Site</a></li>
|
||||
<li><a href="https://community.nodebb.org/category/28/answers" class="dropdown-item rounded-1" target="_blank">Answers</a></li>
|
||||
<li><a href="https://docs.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Documentation</a></li>
|
||||
<li><a href="/bounty" class="dropdown-item rounded-1">Bug Bounty</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/contact" class="btn btn-warning rounded-pill">Contact Us</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="https://manage.nodebb.org/register" class="btn btn-primary"><i class="fa-solid fa-rocket"></i> Start Free Trial</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="container-lg mt-5">
|
||||
<!-- Home -->
|
||||
<div id="home-tab-pane">
|
||||
<div class="row pt-2 pt-lg-5">
|
||||
<div class="col-12 d-flex flex-column gap-4">
|
||||
<h1 class="display-1 fw-bold fs-1">
|
||||
Digitally Overwhelmed Podcast, Episode 140
|
||||
</h1>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- blog post -->
|
||||
<div class="py-5">
|
||||
<div class="d-flex gap-2 align-items-center mb-4">
|
||||
<a href="https://community.nodebb.org/user/julian"><img class="rounded-circle" width="32" src="https://community.nodebb.org/assets/uploads/profile/uid-2/2-profileavatar-1738544541106.jpeg"></a> <a href="https://community.nodebb.org/user/julian" class="fw-semibold">Julian Lam</a> <span class="text-secondary">6/19/2020, 10:00:00 AM</span>
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="kg-card-markdown">
|
||||
|
||||
I had the opportunity to speak on the topic of forum software and its role in the modern web, with my good friend Cinthia Pacheco of <a href="https://digitalbloomiq.com/">Digital Bloom IQ</a>. Cinthia and I go way back, to our high school days when we were hacking away (in the most non-criminal sense of the word, of course) at visual basic in our Computer Sciences class.
|
||||
|
||||
In a modern web dominated by social media giants like Facebook and Twitter, how does a forum solution fit in with an existing brand strategy? How can companies both small and large harness the benefits of community-building without adding <em>yet another tool</em> to manage and maintain?
|
||||
|
||||
Cinthia and I speak on these topics and more in...
|
||||
|
||||
?? <a href="http://digitalbloomiq.com/pod/nodebb"><strong>Episode 140 of the Digitally Overwhelmed podcast</strong></a> ??
|
||||
|
||||
<hr />
|
||||
|
||||
<h3 id="moreaboutcinthia">More about Cinthia...</h3>
|
||||
<img style="float: right; margin: 0 0 1.5em 1.5em;" src="https://nodebb.wpcomstaging.com/wp-content/uploads/2021/08/digitally-overwhelmed-podcast-episode-140.jpg" alt="Digitally Overwhelmed Podcast, Episode 140" /> Cinthia is owner/founder of Digital Bloom IQ and is passionate about helping Health and Wellness businesses heal more of the world through SEO (Search Engine Optimization). After four years of corporate experience working with companies like Avon, Sears, and Hyundai, she transitioned into the small business world, focusing on her SEO and Google Analytics services. She is on a mission to inspire Health and Wellness business to be more intentional about their SEO marketing and share more of the healing talents.
|
||||
|
||||
When she’s not working, you can find her hanging out with her cats, dogs, & boyfriend outside or journaling on her couch.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-5"/>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<div class="d-flex flex-column flex-md-row gap-5 justify-content-between mb-5 flex-wrap">
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Get Started</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/product" class="link-secondary text-decoration-none">PRODUCT</a></li>
|
||||
<li><a href="/pricing" class="link-secondary text-decoration-none">PRICING</a></li>
|
||||
<li><a href="/services" class="link-secondary text-decoration-none">SERVICES</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Resources</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="https://community.nodebb.org/" class="link-secondary text-decoration-none" target="_blank">COMMUNITY</a></li>
|
||||
<li><a href="https://try.nodebb.org/" class="link-secondary text-decoration-none" target="_blank">DEMO SITE</a></li>
|
||||
<li><a href="https://community.nodebb.org/category/28/answers" class="link-secondary text-decoration-none" target="_blank">ANSWERS</a></li>
|
||||
<li><a href="https://docs.nodebb.org" class="link-secondary text-decoration-none" target="_blank">DOCUMENTATION</a></li>
|
||||
<li><a href="/bounty" class="link-secondary text-decoration-none">BUG BOUNTY</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Company</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/about" class="link-secondary text-decoration-none">ABOUT</a></li>
|
||||
<li><a href="/blog" class="link-secondary text-decoration-none">BLOG</a></li>
|
||||
<li><a href="/contact" class="link-secondary text-decoration-none">CONTACT</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<a href="https://manage.nodebb.org/register" class="btn btn-primary"><i class="fa-solid fa-rocket"></i> Start Free Trial</a>
|
||||
<div class="d-flex gap-3 mt-3 justify-content-between px-2">
|
||||
<a title="NodeBB Github Page" href="https://github.com/nodebb/nodebb" class="link-secondary text-decoration-none"><i class="fab fa-github"></i></a>
|
||||
<a title="NodeBB Twitter Page" href="https://twitter.com/nodebb" class="link-secondary text-decoration-none"><i class="fab fa-twitter"></i></a>
|
||||
<a title="NodeBB Mastodon Page" href="https://fosstodon.org/@nodebb" class="link-secondary text-decoration-none"><i class="fa-brands fa-mastodon"></i></a>
|
||||
<a title="NodeBB Facebook Page" href="https://www.facebook.com/NodeBB" class="link-secondary text-decoration-none"><i class="fab fa-facebook"></i></a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap justify-content-between gap-5 small">
|
||||
<span class="text-secondary text-nowrap">©2025 NodeBB, Inc. — Made in Canada with <i class="fa-solid fa-heart text-danger"></i>.</span>
|
||||
<div class="d-flex gap-3">
|
||||
<a href="/tos" class="link-secondary text-nowrap text-decoration-none">Terms of Service</a>
|
||||
<a href="/privacy" class="link-secondary text-nowrap text-decoration-none">Privacy Policy</a>
|
||||
<a href="/gdpr" class="link-secondary text-nowrap text-decoration-none">GDPR</a>
|
||||
<a href="/dmca" class="link-secondary text-nowrap text-decoration-none">DMCA</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap 5 JS Bundle (includes Popper) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
203
blog/forums-and-the-new-era-of-elearning.html
Normal file
|
@ -0,0 +1,203 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Forums and the new era of eLearning - NodeBB - Modern Community Forum Software</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Blog of NodeBB Forum Software - The Modern Discussion Platform">
|
||||
<meta name="author" content="NodeBB Inc.">
|
||||
<meta name="keywords" content="nodebb, node.js, forum, discussion, community, software, hosting, blog">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/icons/32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/icons/16x16.png">
|
||||
<link rel="shortcut icon" href="/images/icons/favicon.ico">
|
||||
|
||||
<!-- Google Fonts: Inter & Poppins -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Poppins:wght@400;600&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<!-- our css-->
|
||||
<link href="/css/style.css" rel="stylesheet">
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-V0P62EB8Q6"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-V0P62EB8Q6');
|
||||
</script>
|
||||
</head>
|
||||
<body class="p-0 py-5 p-lg-5">
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar navbar-expand-lg bg-body fixed-top shadow-sm">
|
||||
<div class="container-lg">
|
||||
<a class="navbar-brand py-2" href="/">
|
||||
<img src="/images/brand/nodebb-logo.svg" style="height: 36px; width: auto;" alt="NodeBB Logo" />
|
||||
</a>
|
||||
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMenu" aria-controls="navbarMenu" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse justify-content-end" id="navbarMenu">
|
||||
<ul class="nav nav-underline gap-4 flex-column flex-lg-row align-items-end align-items-lg-center mt-4 mt-lg-0">
|
||||
<li class="nav-item">
|
||||
<a href="/" class="nav-link text-reset fw-semibold">HOME</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/product" class="nav-link text-reset fw-semibold">PRODUCT</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/pricing" class="nav-link text-reset fw-semibold">PRICING</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/services" class="nav-link text-reset fw-semibold">SERVICES</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" target="_blank" role="button" data-bs-toggle="dropdown" aria-expanded="false" class="nav-link dropdown-toggle text-reset fw-semibold">RESOURCES</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end border-0 shadow p-1">
|
||||
<li><a href="https://community.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Community</a></li>
|
||||
<li><a href="https://try.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Demo Site</a></li>
|
||||
<li><a href="https://community.nodebb.org/category/28/answers" class="dropdown-item rounded-1" target="_blank">Answers</a></li>
|
||||
<li><a href="https://docs.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Documentation</a></li>
|
||||
<li><a href="/bounty" class="dropdown-item rounded-1">Bug Bounty</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/contact" class="btn btn-warning rounded-pill">Contact Us</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="https://manage.nodebb.org/register" class="btn btn-primary"><i class="fa-solid fa-rocket"></i> Start Free Trial</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="container-lg mt-5">
|
||||
<!-- Home -->
|
||||
<div id="home-tab-pane">
|
||||
<div class="row pt-2 pt-lg-5">
|
||||
<div class="col-12 d-flex flex-column gap-4">
|
||||
<h1 class="display-1 fw-bold fs-1">
|
||||
Forums and the new era of eLearning
|
||||
</h1>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- blog post -->
|
||||
<div class="py-5">
|
||||
<div class="d-flex gap-2 align-items-center mb-4">
|
||||
<a href="https://community.nodebb.org/user/julian"><img class="rounded-circle" width="32" src="https://community.nodebb.org/assets/uploads/profile/uid-2/2-profileavatar-1738544541106.jpeg"></a> <a href="https://community.nodebb.org/user/julian" class="fw-semibold">Julian Lam</a> <span class="text-secondary">9/9/2020, 3:30:01 PM</span>
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="kg-card-markdown">
|
||||
|
||||
<p>The ongoing coronavirus pandemic has changed the way we view eLearning. With students preparing to go back to school, parents, teachers, and students alike are wondering how this year will shape up.</p>
|
||||
|
||||
<p>In Ontario, Doug Ford's conversative government had already made strides towards remote eLearning even before the first case of COVID-19 was diagnosed in China. Of course, the priorities then were all about finding efficiencies in government, and while trimming the fat will always be popular with a conservative govenrment, it is at least <em>a little</em> ironic that likely <strong>more</strong> money is now being spent ensuring that students can proceed with this coming year uninterrupted.</p>
|
||||
|
||||
<p>As cities, provinces, and countries re-open, more attention is being given to academic, and how entire cohorts of students must be accommodated in this new era.</p>
|
||||
|
||||
<p>George Veletsianos, a professor in the school of education and technology at Royal Roads University and Canada Research Chair in Innovative Learning and Technology, had this to say about the differences between in-person and eLearning strategies:</p>
|
||||
<blockquote>the meta-analyses have found outcomes between the two are generally the same. If there’s any sort of difference, it tends to favour blended courses.</blockquote>
|
||||
|
||||
<p>As aptly put by Macleans' staff writer Stacy Lee Kong:</p>
|
||||
|
||||
<blockquote>The recent pivot to remote learning during the pandemic was ad hoc, inconsistent and happened during a time of great emotional upheaval for students</blockquote>
|
||||
<p>(The article about this, <a href="https://www.macleans.ca/education/why-learning-from-home-is-an-unlikely-training-ground-for-a-post-pandemic-world/">you can find here on the Macleans website</a>)</p>
|
||||
|
||||
<p>It's no surprise that academic institutions everywhere are looking for best practices in a field that has had much less development, relative to in-person teaching.</p>
|
||||
|
||||
<p>However, there have been many success stories related to online education, such as Khan Academy and Udemy. It is clearly a viable way to teach, and these services arguably become more valuable the longer we're in the throes of this pandemic.</p>
|
||||
|
||||
<p>I had the opportunity to talk to <a href="https://www.itpro.tv/edutainers/">Don Pezet, CEO of ITProTV</a>, about how they are handling the coronavirus pandemic, and asked him to share his thoughts on eLearning at large.</p>
|
||||
|
||||
<strong>If you were to give one piece of advice to academic institutions looking to quickly adopt to remote learning, what would it be?</strong>
|
||||
|
||||
<blockquote>The key to remote learning is maintaining a "presence" with your learners. They have an expectation of being able to see and interact with their teachers. Your online platform needs to replicate this as closely as possible, while remaining accessible to all your students. Synchronous video conferencing is the closest reproduction of the classroom experience, but it is not realistic for many organizations especially when dealing with underprivileged students. Online chat and forum platforms are an ideal middle ground where you can directly interact with your students without needing special equipment, large amounts of bandwidth, or scheduled meeting times.</blockquote>
|
||||
<strong>What pitfalls would you caution against as academic institutions try to implement something quickly in time for September?</strong>
|
||||
<blockquote>The biggest pitfall is trying to do too much all at once. This leads to a lot of complexity for the teachers and students who end up spending more time struggling with the platform instead of learning. Start out small. Establish the minimum viable product needed to educate your students. Then, as people settle in you can start to integrate more advanced technologies like synchronous video.</blockquote>
|
||||
<strong>ITPro.TV combines video-based lectures with a forum component for follow-up questions and collaboration. Do you feel this model works well compared to dedicates courseware solutions?</strong>
|
||||
<blockquote>We cater to audiences around the globe. It is easy for learners in the Americas to tune in to watch our training live and ask our Edutainers questions right there on the spot. However, that leaves out two big demographics: Everyone outside of the Americas, and everyone watching our previously recorded training. We want all of our learners to have a chance to interact with an Edutainer to get their questions answered and help build a personal relationship between the teacher and the student. Our forums directly support that. People all over the world, in any time zone, can post questions and have conversations not only with our Edutainers, but with other people who are studying the same material. This helps to build a community and create a support structure for each person as they study.</blockquote>
|
||||
|
||||
<hr />
|
||||
|
||||
<p>Thanks again to Don for providing such valuable insight!</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<ul>
|
||||
<li>Photo by <a href="https://unsplash.com/@marvelous?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Marvin Meyer</a> on <a href="https://unsplash.com/s/photos/digital?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-5"/>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<div class="d-flex flex-column flex-md-row gap-5 justify-content-between mb-5 flex-wrap">
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Get Started</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/product" class="link-secondary text-decoration-none">PRODUCT</a></li>
|
||||
<li><a href="/pricing" class="link-secondary text-decoration-none">PRICING</a></li>
|
||||
<li><a href="/services" class="link-secondary text-decoration-none">SERVICES</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Resources</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="https://community.nodebb.org/" class="link-secondary text-decoration-none" target="_blank">COMMUNITY</a></li>
|
||||
<li><a href="https://try.nodebb.org/" class="link-secondary text-decoration-none" target="_blank">DEMO SITE</a></li>
|
||||
<li><a href="https://community.nodebb.org/category/28/answers" class="link-secondary text-decoration-none" target="_blank">ANSWERS</a></li>
|
||||
<li><a href="https://docs.nodebb.org" class="link-secondary text-decoration-none" target="_blank">DOCUMENTATION</a></li>
|
||||
<li><a href="/bounty" class="link-secondary text-decoration-none">BUG BOUNTY</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Company</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/about" class="link-secondary text-decoration-none">ABOUT</a></li>
|
||||
<li><a href="/blog" class="link-secondary text-decoration-none">BLOG</a></li>
|
||||
<li><a href="/contact" class="link-secondary text-decoration-none">CONTACT</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<a href="https://manage.nodebb.org/register" class="btn btn-primary"><i class="fa-solid fa-rocket"></i> Start Free Trial</a>
|
||||
<div class="d-flex gap-3 mt-3 justify-content-between px-2">
|
||||
<a title="NodeBB Github Page" href="https://github.com/nodebb/nodebb" class="link-secondary text-decoration-none"><i class="fab fa-github"></i></a>
|
||||
<a title="NodeBB Twitter Page" href="https://twitter.com/nodebb" class="link-secondary text-decoration-none"><i class="fab fa-twitter"></i></a>
|
||||
<a title="NodeBB Mastodon Page" href="https://fosstodon.org/@nodebb" class="link-secondary text-decoration-none"><i class="fa-brands fa-mastodon"></i></a>
|
||||
<a title="NodeBB Facebook Page" href="https://www.facebook.com/NodeBB" class="link-secondary text-decoration-none"><i class="fab fa-facebook"></i></a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap justify-content-between gap-5 small">
|
||||
<span class="text-secondary text-nowrap">©2025 NodeBB, Inc. — Made in Canada with <i class="fa-solid fa-heart text-danger"></i>.</span>
|
||||
<div class="d-flex gap-3">
|
||||
<a href="/tos" class="link-secondary text-nowrap text-decoration-none">Terms of Service</a>
|
||||
<a href="/privacy" class="link-secondary text-nowrap text-decoration-none">Privacy Policy</a>
|
||||
<a href="/gdpr" class="link-secondary text-nowrap text-decoration-none">GDPR</a>
|
||||
<a href="/dmca" class="link-secondary text-nowrap text-decoration-none">DMCA</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap 5 JS Bundle (includes Popper) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
After Width: | Height: | Size: 106 KiB |
BIN
blog/images/digitally-overwhelmed-podcast-episode-140.png
Normal file
After Width: | Height: | Size: 60 KiB |
After Width: | Height: | Size: 80 KiB |
BIN
blog/images/nodebb-1-15-0-home-again-but-still-hard-at-work.jpg
Normal file
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 445 KiB |
After Width: | Height: | Size: 149 KiB |
BIN
blog/images/optimizing-benchpress-2.png
Normal file
After Width: | Height: | Size: 79 KiB |
BIN
blog/images/optimizing-benchpress-3.png
Normal file
After Width: | Height: | Size: 148 KiB |
BIN
blog/images/optimizing-benchpress-4.png
Normal file
After Width: | Height: | Size: 61 KiB |
104
blog/index.html
|
@ -267,6 +267,110 @@
|
|||
<p class="card-text">It’s been a weird and wacky summer but we hope you are hanging in there — my daughter sure was at this cool ropes course recently! Meanwhile the team has...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div><div>
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<a href="https://nodebb.org/blog/what-does-a-forum-migration-look-like-just-ask-moz"><img style="height: 225px; object-fit: cover;" src="https://images.unsplash.com/photo-1533460004989-cef01064af7e?q=80&w=1740&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" class="card-img-top"></img></a>
|
||||
<div class="card-body">
|
||||
<a href="https://nodebb.org/blog/what-does-a-forum-migration-look-like-just-ask-moz" class="card-title fs-5 fw-semibold text-decoration-none">What does a forum migration look like? Just ask Moz...</a>
|
||||
<p class="card-text">We were recently engaged by the SEO services company Moz to re-vamp their Q&A forum. As is typical of many of our client engagements, not only do we do our...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div><div>
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<a href="https://nodebb.org/blog/nodebb-v1-17-0-scheduled-topics-new-moderation-features-and-more"><img style="height: 225px; object-fit: cover;" src="https://nodebb.org/blog/images/nodebb-v1-17-0-scheduled-topics-new-moderation-features-and-more.jpg" class="card-img-top"></img></a>
|
||||
<div class="card-body">
|
||||
<a href="https://nodebb.org/blog/nodebb-v1-17-0-scheduled-topics-new-moderation-features-and-more" class="card-title fs-5 fw-semibold text-decoration-none">NodeBB v1.17.0 – Scheduled Topics, New Moderation Features and More</a>
|
||||
<p class="card-text">Spring has sprung in Toronto, so we’re taking advantage by getting some extremely low budget advertising. Hey, doesn’t everyone make their software purchasing decisions based on what they read on...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div><div>
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<a href="https://nodebb.org/blog/nodebb-v1-16-0-one-last-release-for-a-weird-year"><img style="height: 225px; object-fit: cover;" src="https://nodebb.org/blog/images/nodebb-v1-16-0-one-last-release-for-a-weird-year-2048x1536.jpg" class="card-img-top"></img></a>
|
||||
<div class="card-body">
|
||||
<a href="https://nodebb.org/blog/nodebb-v1-16-0-one-last-release-for-a-weird-year" class="card-title fs-5 fw-semibold text-decoration-none">NodeBB v1.16.0 – One Last Release For A Weird Year</a>
|
||||
<p class="card-text">It’s not news to say 2020 has been… challenging. In Toronto, the home of NodeBB HQ, we’ve gone from a spring lockdown to a cautious summer reopening, to lockdown again...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div><div>
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<a href="https://nodebb.org/blog/the-api-continues-to-evolve"><img style="height: 225px; object-fit: cover;" src="https://images.unsplash.com/photo-1742198810079-49bb51d1c5af?q=80&w=1969&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" class="card-img-top"></img></a>
|
||||
<div class="card-body">
|
||||
<a href="https://nodebb.org/blog/the-api-continues-to-evolve" class="card-title fs-5 fw-semibold text-decoration-none">The API continues to evolve...</a>
|
||||
<p class="card-text">A couple months back as part of our Roadmap to v2, I made the claim that one of the large features in that release would be the merging of the...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div><div>
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<a href="https://nodebb.org/blog/optimizing-benchpress"><img style="height: 225px; object-fit: cover;" src="https://images.unsplash.com/photo-1572044162444-ad60f128bdea?q=80&w=1740&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" class="card-img-top"></img></a>
|
||||
<div class="card-body">
|
||||
<a href="https://nodebb.org/blog/optimizing-benchpress" class="card-title fs-5 fw-semibold text-decoration-none">Optimizing Benchpress</a>
|
||||
<p class="card-text">Optimizing Benchpress Recently, I saw the release of nom v6 and decided I wanted to try it out, and see if I could speed up my hobby JS template compiler,...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div><div>
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<a href="https://nodebb.org/blog/nodebb-1-15-0-home-again-but-still-hard-at-work"><img style="height: 225px; object-fit: cover;" src="nodebb-1-15-0-home-again-but-still-hard-at-work.jpg" class="card-img-top"></img></a>
|
||||
<div class="card-body">
|
||||
<a href="https://nodebb.org/blog/nodebb-1-15-0-home-again-but-still-hard-at-work" class="card-title fs-5 fw-semibold text-decoration-none">NodeBB 1.15.0 - Home Again But Still Hard At Work</a>
|
||||
<p class="card-text">Unfortunately, after only returning a couple of times to our Toronto office, the city started to see a new spike in COVID-19 cases, so we’re back to working remotely. But...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div><div>
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<a href="https://nodebb.org/blog/forums-and-the-new-era-of-elearning"><img style="height: 225px; object-fit: cover;" src="https://images.unsplash.com/photo-1519389950473-47ba0277781c?q=80&w=1740&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" class="card-img-top"></img></a>
|
||||
<div class="card-body">
|
||||
<a href="https://nodebb.org/blog/forums-and-the-new-era-of-elearning" class="card-title fs-5 fw-semibold text-decoration-none">Forums and the new era of eLearning</a>
|
||||
<p class="card-text">The ongoing coronavirus pandemic has changed the way we view eLearning. With students preparing to go back to school, parents, teachers, and students alike are wondering how this year will...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div><div>
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<a href="https://nodebb.org/blog/nodebb-v1-14-3-a-critical-security-update"><img style="height: 225px; object-fit: cover;" src="https://images.unsplash.com/photo-1516116412344-6663387e8590?q=80&w=1828&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" class="card-img-top"></img></a>
|
||||
<div class="card-body">
|
||||
<a href="https://nodebb.org/blog/nodebb-v1-14-3-a-critical-security-update" class="card-title fs-5 fw-semibold text-decoration-none">NodeBB v1.14.3: A Critical Security Update</a>
|
||||
<p class="card-text">A bug in our validation logic made it possible to change the password of any user on a running NodeBB forum by sending a specially crafted socket.io call to the...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div><div>
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<a href="https://nodebb.org/blog/a-decade-in-the-industry-a-ten-year-retrospective"><img style="height: 225px; object-fit: cover;" src="https://nodebb.org/blog/images/a-decade-in-the-industry-a-ten-year-retrospective.jpg" class="card-img-top"></img></a>
|
||||
<div class="card-body">
|
||||
<a href="https://nodebb.org/blog/a-decade-in-the-industry-a-ten-year-retrospective" class="card-title fs-5 fw-semibold text-decoration-none">A decade in the industry; a ten year retrospective</a>
|
||||
<p class="card-text">Recently, a milestone had passed me by without my knowing – the original start date for my very first programming job was May 3rd, 2010. Ten years have flown by...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div><div>
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<a href="https://nodebb.org/blog/the-faceless-master"><img style="height: 225px; object-fit: cover;" src="https://images.unsplash.com/photo-1496631488200-c0b85f3044a7?q=80&w=1740&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" class="card-img-top"></img></a>
|
||||
<div class="card-body">
|
||||
<a href="https://nodebb.org/blog/the-faceless-master" class="card-title fs-5 fw-semibold text-decoration-none">The Faceless Master</a>
|
||||
<p class="card-text">This past month, my family asked me to design a website to showcase my late grandfather’s paintings. Grandpa passed away in ’93, the same year that I was born. We...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div><div>
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<a href="https://nodebb.org/blog/nodebb-1-14-0-distance-wont-keep-us-from-moving-forward"><img style="height: 225px; object-fit: cover;" src="https://nodebb.org/blog/images/nodebb-1-14-0-distance-wont-keep-us-from-moving-forward.jpg" class="card-img-top"></img></a>
|
||||
<div class="card-body">
|
||||
<a href="https://nodebb.org/blog/nodebb-1-14-0-distance-wont-keep-us-from-moving-forward" class="card-title fs-5 fw-semibold text-decoration-none">NodeBB 1.14.0 – Distance Won’t Keep Us From Moving Forward</a>
|
||||
<p class="card-text">It’s been several months since our Toronto team has convened in our downtown office, and we’ve blogged previously about how we are spending our free time during social distancing. But...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div><div>
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<a href="https://nodebb.org/blog/digitally-overwhelmed-podcast-episode-140"><img style="height: 225px; object-fit: cover;" src="https://nodebb.org/blog/images/digitally-overwhelmed-podcast-episode-140.jpg" class="card-img-top"></img></a>
|
||||
<div class="card-body">
|
||||
<a href="https://nodebb.org/blog/digitally-overwhelmed-podcast-episode-140" class="card-title fs-5 fw-semibold text-decoration-none">Digitally Overwhelmed Podcast, Episode 140</a>
|
||||
<p class="card-text">I had the opportunity to speak on the topic of forum software and its role in the modern web, with my good friend Cinthia Pacheco of Digital Bloom IQ. Cinthia...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div><div>
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<a href="https://nodebb.org/blog/unveiling-of-the-read-api"><img style="height: 225px; object-fit: cover;" src="https://images.unsplash.com/photo-1544716278-e513176f20b5?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" class="card-img-top"></img></a>
|
||||
<div class="card-body">
|
||||
<a href="https://nodebb.org/blog/unveiling-of-the-read-api" class="card-title fs-5 fw-semibold text-decoration-none">Unveiling of the Read API</a>
|
||||
<p class="card-text">Developer empowerment has always been at the core of NodeBB: Plugins receive first-class treatment, in that there are plenty of hooks enabling plugins to interact with nearly all facets of...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div><div>
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<a href="https://nodebb.org/blog/looking-ahead-to-nodebb-v2-x"><img style="height: 225px; object-fit: cover;" src="https://nodebb.org/blog/images/looking-ahead-to-nodebb-v2-x-768x493.png" class="card-img-top"></img></a>
|
||||
|
|
|
@ -0,0 +1,197 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>NodeBB 1.14.0 – Distance Won’t Keep Us From Moving Forward - NodeBB - Modern Community Forum Software</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Blog of NodeBB Forum Software - The Modern Discussion Platform">
|
||||
<meta name="author" content="NodeBB Inc.">
|
||||
<meta name="keywords" content="nodebb, node.js, forum, discussion, community, software, hosting, blog">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/icons/32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/icons/16x16.png">
|
||||
<link rel="shortcut icon" href="/images/icons/favicon.ico">
|
||||
|
||||
<!-- Google Fonts: Inter & Poppins -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Poppins:wght@400;600&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<!-- our css-->
|
||||
<link href="/css/style.css" rel="stylesheet">
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-V0P62EB8Q6"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-V0P62EB8Q6');
|
||||
</script>
|
||||
</head>
|
||||
<body class="p-0 py-5 p-lg-5">
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar navbar-expand-lg bg-body fixed-top shadow-sm">
|
||||
<div class="container-lg">
|
||||
<a class="navbar-brand py-2" href="/">
|
||||
<img src="/images/brand/nodebb-logo.svg" style="height: 36px; width: auto;" alt="NodeBB Logo" />
|
||||
</a>
|
||||
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMenu" aria-controls="navbarMenu" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse justify-content-end" id="navbarMenu">
|
||||
<ul class="nav nav-underline gap-4 flex-column flex-lg-row align-items-end align-items-lg-center mt-4 mt-lg-0">
|
||||
<li class="nav-item">
|
||||
<a href="/" class="nav-link text-reset fw-semibold">HOME</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/product" class="nav-link text-reset fw-semibold">PRODUCT</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/pricing" class="nav-link text-reset fw-semibold">PRICING</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/services" class="nav-link text-reset fw-semibold">SERVICES</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" target="_blank" role="button" data-bs-toggle="dropdown" aria-expanded="false" class="nav-link dropdown-toggle text-reset fw-semibold">RESOURCES</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end border-0 shadow p-1">
|
||||
<li><a href="https://community.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Community</a></li>
|
||||
<li><a href="https://try.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Demo Site</a></li>
|
||||
<li><a href="https://community.nodebb.org/category/28/answers" class="dropdown-item rounded-1" target="_blank">Answers</a></li>
|
||||
<li><a href="https://docs.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Documentation</a></li>
|
||||
<li><a href="/bounty" class="dropdown-item rounded-1">Bug Bounty</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/contact" class="btn btn-warning rounded-pill">Contact Us</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="https://manage.nodebb.org/register" class="btn btn-primary"><i class="fa-solid fa-rocket"></i> Start Free Trial</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="container-lg mt-5">
|
||||
<!-- Home -->
|
||||
<div id="home-tab-pane">
|
||||
<div class="row pt-2 pt-lg-5">
|
||||
<div class="col-12 d-flex flex-column gap-4">
|
||||
<h1 class="display-1 fw-bold fs-1">
|
||||
NodeBB 1.14.0 – Distance Won’t Keep Us From Moving Forward
|
||||
</h1>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- blog post -->
|
||||
<div class="py-5">
|
||||
<div class="d-flex gap-2 align-items-center mb-4">
|
||||
<a href="https://community.nodebb.org/user/jay-moonah"><img class="rounded-circle" width="32" src="https://community.nodebb.org/assets/uploads/profile/uid-7950/7950-profileavatar-1701887954691.jpeg"></a> <a href="https://community.nodebb.org/user/jay-moonah" class="fw-semibold">Jay Moonah</a> <span class="text-secondary">7/3/2020, 11:18:01 AM</span>
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="kg-card-markdown">
|
||||
|
||||
It's been several months since our Toronto team has convened in our downtown office, and we've blogged previously about how we are <a href="https://blog.nodebb.org/social-distancing-whats-that/">spending our free time during social distancing</a>. But of course that DOES NOT mean the team hasn't been working hard. Version 1.14.0 includes expanded documentation and improved features, as well as streamlining and security fixes. Here are some highlights:
|
||||
<h2 id="readapidocs">Read API Docs</h2>
|
||||
Since the begining, NodeBB has had a powerful Read API which allows for the JSON for almost every route to be be shown by simply appending "/api" into the path (e.g. the code behind the most recent posts on our own forum can be see at <a href="https://community.nodebb.org/api/recent">https://community.nodebb.org/api/recent</a>). A <a href="https://blog.nodebb.org/unveiling-of-the-read-api/">previous blog post talked in depth about the work being done to document this better</a>, and you can now find a proper list of these routes at:
|
||||
|
||||
<a href="https://docs.nodebb.org/api/">https://docs.nodebb.org/api/</a>
|
||||
<h2 id="moderationimprovements">Moderation Improvements</h2>
|
||||
The 1.14.0 includes a number of improvements to moderation tasks, including better tools for moderators and admins as well as better messaging for users. These include:
|
||||
<ul>
|
||||
<li>New moderation options on the flags detail page: quick assign, ban user, and delete user</li>
|
||||
<li>Better messaging for account deletion (i.e. deletion via account page only deletes account, not content)</li>
|
||||
<li>Admins are now able to choose between deletion of account, content, or both, in account page and ACP.</li>
|
||||
<li>An improved dialog for topic merging, allowing for searching of topics and choosing which topic to merge into</li>
|
||||
<li>Post history can now be restored at the press of a button -- a restored version is just a new edit, so history is preserved</li>
|
||||
</ul>
|
||||
<h2 id="morepowerinprivileges">More Power in Privileges</h2>
|
||||
NodeBB's privileges system has allowed for individual users or groups to be given a wide variety of reading, posting and moderation abilities globally or within specific categories of a forum. With the latest release, admins are now able to grant more fine-grained access control to the Admin Control Panel, including the dashboard, category management, privileges, user management, and forum settings
|
||||
<ul>
|
||||
<li>Privilege grant/rescind now fires plugin hooks for all types, category, global, and admin hooks</li>
|
||||
</ul>
|
||||
<h2 id="otherchanges">Other Changes</h2>
|
||||
<ul>
|
||||
<li>Edits to posts inside watched topics now send notifications</li>
|
||||
<li>User blocks functionality now has plugin hooks</li>
|
||||
<li>Profile export now returns more user data</li>
|
||||
<li>Grunt restart/rebuild and filechange detection have been sped up</li>
|
||||
<li>The General menu has been removed from the Admin Control Panel, with items there mostly moved to Settings</li>
|
||||
</ul>
|
||||
Because this release contains a number of security fixes, we <strong>highly recommend upgrading at your earliest convenience</strong> If you are running your own server and encounter any issues upgrading, please post them in the <a href="https://community.nodebb.org/">community support forum</a>.
|
||||
|
||||
If you are a hosting client of with us, please drop a line to <a href="mailto:support@nodebb.org">support@nodebb.org</a> to schedule a time for our team to perform the upgrade.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-5"/>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<div class="d-flex flex-column flex-md-row gap-5 justify-content-between mb-5 flex-wrap">
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Get Started</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/product" class="link-secondary text-decoration-none">PRODUCT</a></li>
|
||||
<li><a href="/pricing" class="link-secondary text-decoration-none">PRICING</a></li>
|
||||
<li><a href="/services" class="link-secondary text-decoration-none">SERVICES</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Resources</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="https://community.nodebb.org/" class="link-secondary text-decoration-none" target="_blank">COMMUNITY</a></li>
|
||||
<li><a href="https://try.nodebb.org/" class="link-secondary text-decoration-none" target="_blank">DEMO SITE</a></li>
|
||||
<li><a href="https://community.nodebb.org/category/28/answers" class="link-secondary text-decoration-none" target="_blank">ANSWERS</a></li>
|
||||
<li><a href="https://docs.nodebb.org" class="link-secondary text-decoration-none" target="_blank">DOCUMENTATION</a></li>
|
||||
<li><a href="/bounty" class="link-secondary text-decoration-none">BUG BOUNTY</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Company</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/about" class="link-secondary text-decoration-none">ABOUT</a></li>
|
||||
<li><a href="/blog" class="link-secondary text-decoration-none">BLOG</a></li>
|
||||
<li><a href="/contact" class="link-secondary text-decoration-none">CONTACT</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<a href="https://manage.nodebb.org/register" class="btn btn-primary"><i class="fa-solid fa-rocket"></i> Start Free Trial</a>
|
||||
<div class="d-flex gap-3 mt-3 justify-content-between px-2">
|
||||
<a title="NodeBB Github Page" href="https://github.com/nodebb/nodebb" class="link-secondary text-decoration-none"><i class="fab fa-github"></i></a>
|
||||
<a title="NodeBB Twitter Page" href="https://twitter.com/nodebb" class="link-secondary text-decoration-none"><i class="fab fa-twitter"></i></a>
|
||||
<a title="NodeBB Mastodon Page" href="https://fosstodon.org/@nodebb" class="link-secondary text-decoration-none"><i class="fa-brands fa-mastodon"></i></a>
|
||||
<a title="NodeBB Facebook Page" href="https://www.facebook.com/NodeBB" class="link-secondary text-decoration-none"><i class="fab fa-facebook"></i></a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap justify-content-between gap-5 small">
|
||||
<span class="text-secondary text-nowrap">©2025 NodeBB, Inc. — Made in Canada with <i class="fa-solid fa-heart text-danger"></i>.</span>
|
||||
<div class="d-flex gap-3">
|
||||
<a href="/tos" class="link-secondary text-nowrap text-decoration-none">Terms of Service</a>
|
||||
<a href="/privacy" class="link-secondary text-nowrap text-decoration-none">Privacy Policy</a>
|
||||
<a href="/gdpr" class="link-secondary text-nowrap text-decoration-none">GDPR</a>
|
||||
<a href="/dmca" class="link-secondary text-nowrap text-decoration-none">DMCA</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap 5 JS Bundle (includes Popper) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
190
blog/nodebb-1-15-0-home-again-but-still-hard-at-work.html
Normal file
|
@ -0,0 +1,190 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>NodeBB 1.15.0 - Home Again But Still Hard At Work - NodeBB - Modern Community Forum Software</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Blog of NodeBB Forum Software - The Modern Discussion Platform">
|
||||
<meta name="author" content="NodeBB Inc.">
|
||||
<meta name="keywords" content="nodebb, node.js, forum, discussion, community, software, hosting, blog">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/icons/32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/icons/16x16.png">
|
||||
<link rel="shortcut icon" href="/images/icons/favicon.ico">
|
||||
|
||||
<!-- Google Fonts: Inter & Poppins -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Poppins:wght@400;600&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<!-- our css-->
|
||||
<link href="/css/style.css" rel="stylesheet">
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-V0P62EB8Q6"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-V0P62EB8Q6');
|
||||
</script>
|
||||
</head>
|
||||
<body class="p-0 py-5 p-lg-5">
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar navbar-expand-lg bg-body fixed-top shadow-sm">
|
||||
<div class="container-lg">
|
||||
<a class="navbar-brand py-2" href="/">
|
||||
<img src="/images/brand/nodebb-logo.svg" style="height: 36px; width: auto;" alt="NodeBB Logo" />
|
||||
</a>
|
||||
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMenu" aria-controls="navbarMenu" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse justify-content-end" id="navbarMenu">
|
||||
<ul class="nav nav-underline gap-4 flex-column flex-lg-row align-items-end align-items-lg-center mt-4 mt-lg-0">
|
||||
<li class="nav-item">
|
||||
<a href="/" class="nav-link text-reset fw-semibold">HOME</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/product" class="nav-link text-reset fw-semibold">PRODUCT</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/pricing" class="nav-link text-reset fw-semibold">PRICING</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/services" class="nav-link text-reset fw-semibold">SERVICES</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" target="_blank" role="button" data-bs-toggle="dropdown" aria-expanded="false" class="nav-link dropdown-toggle text-reset fw-semibold">RESOURCES</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end border-0 shadow p-1">
|
||||
<li><a href="https://community.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Community</a></li>
|
||||
<li><a href="https://try.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Demo Site</a></li>
|
||||
<li><a href="https://community.nodebb.org/category/28/answers" class="dropdown-item rounded-1" target="_blank">Answers</a></li>
|
||||
<li><a href="https://docs.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Documentation</a></li>
|
||||
<li><a href="/bounty" class="dropdown-item rounded-1">Bug Bounty</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/contact" class="btn btn-warning rounded-pill">Contact Us</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="https://manage.nodebb.org/register" class="btn btn-primary"><i class="fa-solid fa-rocket"></i> Start Free Trial</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="container-lg mt-5">
|
||||
<!-- Home -->
|
||||
<div id="home-tab-pane">
|
||||
<div class="row pt-2 pt-lg-5">
|
||||
<div class="col-12 d-flex flex-column gap-4">
|
||||
<h1 class="display-1 fw-bold fs-1">
|
||||
NodeBB 1.15.0 - Home Again But Still Hard At Work
|
||||
</h1>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- blog post -->
|
||||
<div class="py-5">
|
||||
<div class="d-flex gap-2 align-items-center mb-4">
|
||||
<a href="https://community.nodebb.org/user/jay-moonah"><img class="rounded-circle" width="32" src="https://community.nodebb.org/assets/uploads/profile/uid-7950/7950-profileavatar-1701887954691.jpeg"></a> <a href="https://community.nodebb.org/user/jay-moonah" class="fw-semibold">Jay Moonah</a> <span class="text-secondary">11/8/2020, 5:07:06 PM</span>
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="kg-card-markdown">
|
||||
|
||||
<p>Unfortunately, after only returning a couple of times to our Toronto office, the city started to see a new spike in COVID-19 cases, so we're back to working remotely. But that certainly won't stop us from continuing to improve NodeBB, including fast-tracking a long-planned feature.</p>
|
||||
|
||||
<h2 id="writeapimergedintocore">Write API Merged Into Core</h2>
|
||||
This was originally meant to be a part of version 2.0, but instead we decided to move ahead and bring it out even sooner. For those who had previously been using the Write API plug-in, it should be a simple switch from the <code>v2</code> endpoint to the new <code>v3</code> endpoint, although it is wise to <a href="https://github.com/NodeBB/NodeBB/pull/8708#issuecomment-705780711">check the breaking changes</a> to see if you are affected. Note that you <em>can</em> continue to use the plugin as it will not conflict with the new API. Eventually we will expand this API with <code>GET</code> routes and make it a complete API, but for now retrieving data from NodeBB can still be done by prepending <code>/api</code> to all page routes. (As a reminder, we recently released a list of all routes at <a href="https://docs.nodebb.org/api/">https://docs.nodebb.org/api/</a>).
|
||||
<h2 id="flagsimprovements">Flags Improvements</h2>
|
||||
The previous release included a number of moderation improvements, and we've continued the work on flags to make it more usable:
|
||||
<ul>
|
||||
<li>Multiple flag reports are now consolidated into a single flag, so duplicate reports for the same piece of content are now simply added to the existing flag rather than generating multiple reports</li>
|
||||
<li>Flags page has been updated to allow for additional functionality (sorting, and bulk actions)</li>
|
||||
</ul>
|
||||
<h2 id="otherchanges">Other Changes</h2>
|
||||
<ul>
|
||||
<li>Progressive Web Application enhancements – NodeBB is now "installable" from mobile devices to your home screen, and should act like an app now
|
||||
** This is one of the starting points to eventually allowing NodeBB to be accessible while offline, just like a native app</li>
|
||||
<li>"Verified Users" is now a separate system group, allowing you to segment privileges and control category access to those specific users
|
||||
** Verified users are those users who have confirmed (or "verified") their email address</li>
|
||||
<li>Able to search a particular categories including its children
|
||||
** Before, you had to select all of the categories, including its children</li>
|
||||
<li>And finally because we're always thinking about performance, Baris Usakli has made a number of optimizations that should allow NodeBB to handle more traffic</li>
|
||||
</ul>
|
||||
Be sure to check out the breaking changes at <a href="https://community.nodebb.org/topic/14917/1-15-0-breaking-changes">https://community.nodebb.org/topic/14917/1-15-0-breaking-changes</a>. As always we <strong>recommend backing up your database, and upgrading at your earliest convenience</strong>. If you are running your own server and encounter any issues, please drop a post on the <a href="https://community.nodebb.org/">community support forum</a>.
|
||||
|
||||
If you are a hosting client of with us, please drop a line to <a href="mailto:support@nodebb.org">support@nodebb.org</a> to schedule a time for our team to perform the upgrade.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-5"/>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<div class="d-flex flex-column flex-md-row gap-5 justify-content-between mb-5 flex-wrap">
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Get Started</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/product" class="link-secondary text-decoration-none">PRODUCT</a></li>
|
||||
<li><a href="/pricing" class="link-secondary text-decoration-none">PRICING</a></li>
|
||||
<li><a href="/services" class="link-secondary text-decoration-none">SERVICES</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Resources</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="https://community.nodebb.org/" class="link-secondary text-decoration-none" target="_blank">COMMUNITY</a></li>
|
||||
<li><a href="https://try.nodebb.org/" class="link-secondary text-decoration-none" target="_blank">DEMO SITE</a></li>
|
||||
<li><a href="https://community.nodebb.org/category/28/answers" class="link-secondary text-decoration-none" target="_blank">ANSWERS</a></li>
|
||||
<li><a href="https://docs.nodebb.org" class="link-secondary text-decoration-none" target="_blank">DOCUMENTATION</a></li>
|
||||
<li><a href="/bounty" class="link-secondary text-decoration-none">BUG BOUNTY</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Company</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/about" class="link-secondary text-decoration-none">ABOUT</a></li>
|
||||
<li><a href="/blog" class="link-secondary text-decoration-none">BLOG</a></li>
|
||||
<li><a href="/contact" class="link-secondary text-decoration-none">CONTACT</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<a href="https://manage.nodebb.org/register" class="btn btn-primary"><i class="fa-solid fa-rocket"></i> Start Free Trial</a>
|
||||
<div class="d-flex gap-3 mt-3 justify-content-between px-2">
|
||||
<a title="NodeBB Github Page" href="https://github.com/nodebb/nodebb" class="link-secondary text-decoration-none"><i class="fab fa-github"></i></a>
|
||||
<a title="NodeBB Twitter Page" href="https://twitter.com/nodebb" class="link-secondary text-decoration-none"><i class="fab fa-twitter"></i></a>
|
||||
<a title="NodeBB Mastodon Page" href="https://fosstodon.org/@nodebb" class="link-secondary text-decoration-none"><i class="fa-brands fa-mastodon"></i></a>
|
||||
<a title="NodeBB Facebook Page" href="https://www.facebook.com/NodeBB" class="link-secondary text-decoration-none"><i class="fab fa-facebook"></i></a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap justify-content-between gap-5 small">
|
||||
<span class="text-secondary text-nowrap">©2025 NodeBB, Inc. — Made in Canada with <i class="fa-solid fa-heart text-danger"></i>.</span>
|
||||
<div class="d-flex gap-3">
|
||||
<a href="/tos" class="link-secondary text-nowrap text-decoration-none">Terms of Service</a>
|
||||
<a href="/privacy" class="link-secondary text-nowrap text-decoration-none">Privacy Policy</a>
|
||||
<a href="/gdpr" class="link-secondary text-nowrap text-decoration-none">GDPR</a>
|
||||
<a href="/dmca" class="link-secondary text-nowrap text-decoration-none">DMCA</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap 5 JS Bundle (includes Popper) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
169
blog/nodebb-v1-14-3-a-critical-security-update.html
Normal file
|
@ -0,0 +1,169 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>NodeBB v1.14.3: A Critical Security Update - NodeBB - Modern Community Forum Software</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Blog of NodeBB Forum Software - The Modern Discussion Platform">
|
||||
<meta name="author" content="NodeBB Inc.">
|
||||
<meta name="keywords" content="nodebb, node.js, forum, discussion, community, software, hosting, blog">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/icons/32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/icons/16x16.png">
|
||||
<link rel="shortcut icon" href="/images/icons/favicon.ico">
|
||||
|
||||
<!-- Google Fonts: Inter & Poppins -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Poppins:wght@400;600&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<!-- our css-->
|
||||
<link href="/css/style.css" rel="stylesheet">
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-V0P62EB8Q6"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-V0P62EB8Q6');
|
||||
</script>
|
||||
</head>
|
||||
<body class="p-0 py-5 p-lg-5">
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar navbar-expand-lg bg-body fixed-top shadow-sm">
|
||||
<div class="container-lg">
|
||||
<a class="navbar-brand py-2" href="/">
|
||||
<img src="/images/brand/nodebb-logo.svg" style="height: 36px; width: auto;" alt="NodeBB Logo" />
|
||||
</a>
|
||||
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMenu" aria-controls="navbarMenu" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse justify-content-end" id="navbarMenu">
|
||||
<ul class="nav nav-underline gap-4 flex-column flex-lg-row align-items-end align-items-lg-center mt-4 mt-lg-0">
|
||||
<li class="nav-item">
|
||||
<a href="/" class="nav-link text-reset fw-semibold">HOME</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/product" class="nav-link text-reset fw-semibold">PRODUCT</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/pricing" class="nav-link text-reset fw-semibold">PRICING</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/services" class="nav-link text-reset fw-semibold">SERVICES</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" target="_blank" role="button" data-bs-toggle="dropdown" aria-expanded="false" class="nav-link dropdown-toggle text-reset fw-semibold">RESOURCES</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end border-0 shadow p-1">
|
||||
<li><a href="https://community.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Community</a></li>
|
||||
<li><a href="https://try.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Demo Site</a></li>
|
||||
<li><a href="https://community.nodebb.org/category/28/answers" class="dropdown-item rounded-1" target="_blank">Answers</a></li>
|
||||
<li><a href="https://docs.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Documentation</a></li>
|
||||
<li><a href="/bounty" class="dropdown-item rounded-1">Bug Bounty</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/contact" class="btn btn-warning rounded-pill">Contact Us</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="https://manage.nodebb.org/register" class="btn btn-primary"><i class="fa-solid fa-rocket"></i> Start Free Trial</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="container-lg mt-5">
|
||||
<!-- Home -->
|
||||
<div id="home-tab-pane">
|
||||
<div class="row pt-2 pt-lg-5">
|
||||
<div class="col-12 d-flex flex-column gap-4">
|
||||
<h1 class="display-1 fw-bold fs-1">
|
||||
NodeBB v1.14.3: A Critical Security Update
|
||||
</h1>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- blog post -->
|
||||
<div class="py-5">
|
||||
<div class="d-flex gap-2 align-items-center mb-4">
|
||||
<a href="https://community.nodebb.org/user/psychobunny"><img class="rounded-circle" width="32" src="https://avatars.githubusercontent.com/u/654670?v=4"></a> <a href="https://community.nodebb.org/user/psychobunny" class="fw-semibold">Andrew Rodrigues</a> <span class="text-secondary">8/17/2020, 6:41:27 PM</span>
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="kg-card-markdown"><p>A bug in our validation logic made it possible to change the password of any user on a running NodeBB forum by sending a specially crafted socket.io call to the server.</p>
|
||||
<p>We have resolved this in the latest version of NodeBB, and the fix has already been rolled out as a patch on all of our hosted customers.</p>
|
||||
<p>For more information on the vulnerability as well as instructions on how to resolve this issue, please have a look here: <a href="https://github.com/NodeBB/NodeBB/security/advisories/GHSA-hr66-c8pg-5mg7">https://github.com/NodeBB/NodeBB/security/advisories/GHSA-hr66-c8pg-5mg7</a></p>
|
||||
<p>If you are unable to upgrade ASAP, you can also apply the patch via cherry-picking <a href="https://github.com/NodeBB/NodeBB/commit/16cee1b03ba3eee177834a1fdac4aa8a12b39d2a">this commit</a>.</p>
|
||||
<p>As this release contains a critical security fix, we <strong>highly recommend upgrading at your earliest convenience</strong>. If you are running your own server and encounter any issues upgrading, please post them in the <a href="https://community.nodebb.org/">community support forum</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-5"/>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<div class="d-flex flex-column flex-md-row gap-5 justify-content-between mb-5 flex-wrap">
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Get Started</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/product" class="link-secondary text-decoration-none">PRODUCT</a></li>
|
||||
<li><a href="/pricing" class="link-secondary text-decoration-none">PRICING</a></li>
|
||||
<li><a href="/services" class="link-secondary text-decoration-none">SERVICES</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Resources</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="https://community.nodebb.org/" class="link-secondary text-decoration-none" target="_blank">COMMUNITY</a></li>
|
||||
<li><a href="https://try.nodebb.org/" class="link-secondary text-decoration-none" target="_blank">DEMO SITE</a></li>
|
||||
<li><a href="https://community.nodebb.org/category/28/answers" class="link-secondary text-decoration-none" target="_blank">ANSWERS</a></li>
|
||||
<li><a href="https://docs.nodebb.org" class="link-secondary text-decoration-none" target="_blank">DOCUMENTATION</a></li>
|
||||
<li><a href="/bounty" class="link-secondary text-decoration-none">BUG BOUNTY</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Company</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/about" class="link-secondary text-decoration-none">ABOUT</a></li>
|
||||
<li><a href="/blog" class="link-secondary text-decoration-none">BLOG</a></li>
|
||||
<li><a href="/contact" class="link-secondary text-decoration-none">CONTACT</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<a href="https://manage.nodebb.org/register" class="btn btn-primary"><i class="fa-solid fa-rocket"></i> Start Free Trial</a>
|
||||
<div class="d-flex gap-3 mt-3 justify-content-between px-2">
|
||||
<a title="NodeBB Github Page" href="https://github.com/nodebb/nodebb" class="link-secondary text-decoration-none"><i class="fab fa-github"></i></a>
|
||||
<a title="NodeBB Twitter Page" href="https://twitter.com/nodebb" class="link-secondary text-decoration-none"><i class="fab fa-twitter"></i></a>
|
||||
<a title="NodeBB Mastodon Page" href="https://fosstodon.org/@nodebb" class="link-secondary text-decoration-none"><i class="fa-brands fa-mastodon"></i></a>
|
||||
<a title="NodeBB Facebook Page" href="https://www.facebook.com/NodeBB" class="link-secondary text-decoration-none"><i class="fab fa-facebook"></i></a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap justify-content-between gap-5 small">
|
||||
<span class="text-secondary text-nowrap">©2025 NodeBB, Inc. — Made in Canada with <i class="fa-solid fa-heart text-danger"></i>.</span>
|
||||
<div class="d-flex gap-3">
|
||||
<a href="/tos" class="link-secondary text-nowrap text-decoration-none">Terms of Service</a>
|
||||
<a href="/privacy" class="link-secondary text-nowrap text-decoration-none">Privacy Policy</a>
|
||||
<a href="/gdpr" class="link-secondary text-nowrap text-decoration-none">GDPR</a>
|
||||
<a href="/dmca" class="link-secondary text-nowrap text-decoration-none">DMCA</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap 5 JS Bundle (includes Popper) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
187
blog/nodebb-v1-16-0-one-last-release-for-a-weird-year.html
Normal file
|
@ -0,0 +1,187 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>NodeBB v1.16.0 – One Last Release For A Weird Year - NodeBB - Modern Community Forum Software</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Blog of NodeBB Forum Software - The Modern Discussion Platform">
|
||||
<meta name="author" content="NodeBB Inc.">
|
||||
<meta name="keywords" content="nodebb, node.js, forum, discussion, community, software, hosting, blog">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/icons/32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/icons/16x16.png">
|
||||
<link rel="shortcut icon" href="/images/icons/favicon.ico">
|
||||
|
||||
<!-- Google Fonts: Inter & Poppins -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Poppins:wght@400;600&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<!-- our css-->
|
||||
<link href="/css/style.css" rel="stylesheet">
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-V0P62EB8Q6"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-V0P62EB8Q6');
|
||||
</script>
|
||||
</head>
|
||||
<body class="p-0 py-5 p-lg-5">
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar navbar-expand-lg bg-body fixed-top shadow-sm">
|
||||
<div class="container-lg">
|
||||
<a class="navbar-brand py-2" href="/">
|
||||
<img src="/images/brand/nodebb-logo.svg" style="height: 36px; width: auto;" alt="NodeBB Logo" />
|
||||
</a>
|
||||
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMenu" aria-controls="navbarMenu" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse justify-content-end" id="navbarMenu">
|
||||
<ul class="nav nav-underline gap-4 flex-column flex-lg-row align-items-end align-items-lg-center mt-4 mt-lg-0">
|
||||
<li class="nav-item">
|
||||
<a href="/" class="nav-link text-reset fw-semibold">HOME</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/product" class="nav-link text-reset fw-semibold">PRODUCT</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/pricing" class="nav-link text-reset fw-semibold">PRICING</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/services" class="nav-link text-reset fw-semibold">SERVICES</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" target="_blank" role="button" data-bs-toggle="dropdown" aria-expanded="false" class="nav-link dropdown-toggle text-reset fw-semibold">RESOURCES</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end border-0 shadow p-1">
|
||||
<li><a href="https://community.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Community</a></li>
|
||||
<li><a href="https://try.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Demo Site</a></li>
|
||||
<li><a href="https://community.nodebb.org/category/28/answers" class="dropdown-item rounded-1" target="_blank">Answers</a></li>
|
||||
<li><a href="https://docs.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Documentation</a></li>
|
||||
<li><a href="/bounty" class="dropdown-item rounded-1">Bug Bounty</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/contact" class="btn btn-warning rounded-pill">Contact Us</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="https://manage.nodebb.org/register" class="btn btn-primary"><i class="fa-solid fa-rocket"></i> Start Free Trial</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="container-lg mt-5">
|
||||
<!-- Home -->
|
||||
<div id="home-tab-pane">
|
||||
<div class="row pt-2 pt-lg-5">
|
||||
<div class="col-12 d-flex flex-column gap-4">
|
||||
<h1 class="display-1 fw-bold fs-1">
|
||||
NodeBB v1.16.0 – One Last Release For A Weird Year
|
||||
</h1>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- blog post -->
|
||||
<div class="py-5">
|
||||
<div class="d-flex gap-2 align-items-center mb-4">
|
||||
<a href="https://community.nodebb.org/user/jay-moonah"><img class="rounded-circle" width="32" src="https://community.nodebb.org/assets/uploads/profile/uid-7950/7950-profileavatar-1701887954691.jpeg"></a> <a href="https://community.nodebb.org/user/jay-moonah" class="fw-semibold">Jay Moonah</a> <span class="text-secondary">12/24/2020, 12:29:11 PM</span>
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="kg-card-markdown">
|
||||
|
||||
<p>It's not news to say 2020 has been... challenging. In Toronto, the home of NodeBB HQ, we've gone from a spring lockdown to a cautious summer reopening, to lockdown again — <em>sigh</em>. But the team has never stopped working, and have managed to squeeze out one last release before we thankfully turn over the calendar.</p>
|
||||
|
||||
<p>Improvements include:</p>
|
||||
<ul>
|
||||
<li><strong>Topic thumbnails.</strong> Multiple topic thumbnails now supported per topic. Theme integration (e.g. Persona) will follow in the near future.</li>
|
||||
</ul>
|
||||
<img src="https://nodebb.wpcomstaging.com/wp-content/uploads/2021/08/nodebb-v1-16-0-one-last-release-for-a-weird-year.png" alt="NodeBB v1.16.0 - One Last Release For A Weird Year" />
|
||||
<ul>
|
||||
<li><strong>Plugins admin.</strong> Plugins are now able to override the ACP relogin challenge. The default behaviour was (and still remains) a re-entering of the password. As an example, the <a href="https://github.com/julianlam/nodebb-plugin-2factor/">Two-Factor Authentication plugin</a> takes advantage of this new functionality by presenting a 2FA challenge instead.</li>
|
||||
<li><strong>Updated topic navigator.</strong> We have added a new topic navigator for those using infinite scrolling! Click the bar at the bottom right to open the navigator and try it out for yourself.</li>
|
||||
</ul>
|
||||
<img src="https://nodebb.wpcomstaging.com/wp-content/uploads/2021/08/nodebb-v1-16-0-one-last-release-for-a-weird-year.gif" alt="NodeBB v1.16.0 - One Last Release For A Weird Year" />
|
||||
|
||||
<p>For a list of breaking changes, please see:</p>
|
||||
|
||||
<a href="https://community.nodebb.org/topic/15178/1-16-0-breaking-changes">https://community.nodebb.org/topic/15178/1-16-0-breaking-changes</a>
|
||||
|
||||
<p>As always if you are hosting your own NodeBB forum we recommend <a href="https://docs.nodebb.org/configuring/upgrade/">updating to the latest version</a> to take advantage of the latest features and fixes. Remember to back up your database before proceeding with any upgrade.</p>
|
||||
|
||||
<p>If you encounter any issues, please post them in our community support forum.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-5"/>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<div class="d-flex flex-column flex-md-row gap-5 justify-content-between mb-5 flex-wrap">
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Get Started</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/product" class="link-secondary text-decoration-none">PRODUCT</a></li>
|
||||
<li><a href="/pricing" class="link-secondary text-decoration-none">PRICING</a></li>
|
||||
<li><a href="/services" class="link-secondary text-decoration-none">SERVICES</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Resources</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="https://community.nodebb.org/" class="link-secondary text-decoration-none" target="_blank">COMMUNITY</a></li>
|
||||
<li><a href="https://try.nodebb.org/" class="link-secondary text-decoration-none" target="_blank">DEMO SITE</a></li>
|
||||
<li><a href="https://community.nodebb.org/category/28/answers" class="link-secondary text-decoration-none" target="_blank">ANSWERS</a></li>
|
||||
<li><a href="https://docs.nodebb.org" class="link-secondary text-decoration-none" target="_blank">DOCUMENTATION</a></li>
|
||||
<li><a href="/bounty" class="link-secondary text-decoration-none">BUG BOUNTY</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Company</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/about" class="link-secondary text-decoration-none">ABOUT</a></li>
|
||||
<li><a href="/blog" class="link-secondary text-decoration-none">BLOG</a></li>
|
||||
<li><a href="/contact" class="link-secondary text-decoration-none">CONTACT</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<a href="https://manage.nodebb.org/register" class="btn btn-primary"><i class="fa-solid fa-rocket"></i> Start Free Trial</a>
|
||||
<div class="d-flex gap-3 mt-3 justify-content-between px-2">
|
||||
<a title="NodeBB Github Page" href="https://github.com/nodebb/nodebb" class="link-secondary text-decoration-none"><i class="fab fa-github"></i></a>
|
||||
<a title="NodeBB Twitter Page" href="https://twitter.com/nodebb" class="link-secondary text-decoration-none"><i class="fab fa-twitter"></i></a>
|
||||
<a title="NodeBB Mastodon Page" href="https://fosstodon.org/@nodebb" class="link-secondary text-decoration-none"><i class="fa-brands fa-mastodon"></i></a>
|
||||
<a title="NodeBB Facebook Page" href="https://www.facebook.com/NodeBB" class="link-secondary text-decoration-none"><i class="fab fa-facebook"></i></a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap justify-content-between gap-5 small">
|
||||
<span class="text-secondary text-nowrap">©2025 NodeBB, Inc. — Made in Canada with <i class="fa-solid fa-heart text-danger"></i>.</span>
|
||||
<div class="d-flex gap-3">
|
||||
<a href="/tos" class="link-secondary text-nowrap text-decoration-none">Terms of Service</a>
|
||||
<a href="/privacy" class="link-secondary text-nowrap text-decoration-none">Privacy Policy</a>
|
||||
<a href="/gdpr" class="link-secondary text-nowrap text-decoration-none">GDPR</a>
|
||||
<a href="/dmca" class="link-secondary text-nowrap text-decoration-none">DMCA</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap 5 JS Bundle (includes Popper) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,214 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>NodeBB v1.17.0 – Scheduled Topics, New Moderation Features and More - NodeBB - Modern Community Forum Software</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Blog of NodeBB Forum Software - The Modern Discussion Platform">
|
||||
<meta name="author" content="NodeBB Inc.">
|
||||
<meta name="keywords" content="nodebb, node.js, forum, discussion, community, software, hosting, blog">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/icons/32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/icons/16x16.png">
|
||||
<link rel="shortcut icon" href="/images/icons/favicon.ico">
|
||||
|
||||
<!-- Google Fonts: Inter & Poppins -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Poppins:wght@400;600&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<!-- our css-->
|
||||
<link href="/css/style.css" rel="stylesheet">
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-V0P62EB8Q6"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-V0P62EB8Q6');
|
||||
</script>
|
||||
</head>
|
||||
<body class="p-0 py-5 p-lg-5">
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar navbar-expand-lg bg-body fixed-top shadow-sm">
|
||||
<div class="container-lg">
|
||||
<a class="navbar-brand py-2" href="/">
|
||||
<img src="/images/brand/nodebb-logo.svg" style="height: 36px; width: auto;" alt="NodeBB Logo" />
|
||||
</a>
|
||||
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMenu" aria-controls="navbarMenu" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse justify-content-end" id="navbarMenu">
|
||||
<ul class="nav nav-underline gap-4 flex-column flex-lg-row align-items-end align-items-lg-center mt-4 mt-lg-0">
|
||||
<li class="nav-item">
|
||||
<a href="/" class="nav-link text-reset fw-semibold">HOME</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/product" class="nav-link text-reset fw-semibold">PRODUCT</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/pricing" class="nav-link text-reset fw-semibold">PRICING</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/services" class="nav-link text-reset fw-semibold">SERVICES</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" target="_blank" role="button" data-bs-toggle="dropdown" aria-expanded="false" class="nav-link dropdown-toggle text-reset fw-semibold">RESOURCES</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end border-0 shadow p-1">
|
||||
<li><a href="https://community.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Community</a></li>
|
||||
<li><a href="https://try.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Demo Site</a></li>
|
||||
<li><a href="https://community.nodebb.org/category/28/answers" class="dropdown-item rounded-1" target="_blank">Answers</a></li>
|
||||
<li><a href="https://docs.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Documentation</a></li>
|
||||
<li><a href="/bounty" class="dropdown-item rounded-1">Bug Bounty</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/contact" class="btn btn-warning rounded-pill">Contact Us</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="https://manage.nodebb.org/register" class="btn btn-primary"><i class="fa-solid fa-rocket"></i> Start Free Trial</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="container-lg mt-5">
|
||||
<!-- Home -->
|
||||
<div id="home-tab-pane">
|
||||
<div class="row pt-2 pt-lg-5">
|
||||
<div class="col-12 d-flex flex-column gap-4">
|
||||
<h1 class="display-1 fw-bold fs-1">
|
||||
NodeBB v1.17.0 – Scheduled Topics, New Moderation Features and More
|
||||
</h1>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- blog post -->
|
||||
<div class="py-5">
|
||||
<div class="d-flex gap-2 align-items-center mb-4">
|
||||
<a href="https://community.nodebb.org/user/jay-moonah"><img class="rounded-circle" width="32" src="https://community.nodebb.org/assets/uploads/profile/uid-7950/7950-profileavatar-1701887954691.jpeg"></a> <a href="https://community.nodebb.org/user/jay-moonah" class="fw-semibold">Jay Moonah</a> <span class="text-secondary">4/26/2021, 4:56:03 PM</span>
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="kg-card-markdown">
|
||||
|
||||
<p>Spring has sprung in Toronto, so we're taking advantage by getting some <em>extremely</em> low budget advertising. Hey, doesn't everyone make their software purchasing decisions based on what they read on the sidewalk? Hah-hah!</p>
|
||||
|
||||
<p>Onward with our latest release, NodeBB version 1.17.0. Improvements include:</p>
|
||||
<ul>
|
||||
<li><strong>Scheduled Topics</strong> We've had multiple requests for this one. Rather than simply submitting a topic immediately users now have the option to select a time and date for it to go live. If you wish to limit this ability to certain users, there's also a new category-level privilege that can toggled for individuals or user groups.</li>
|
||||
<li><strong>Post Diff Deletion</strong> Specific entries in post history can now be removed. This resolved an issue where simply editing a post to remove sensitive content, such as passwords or secret key was not enough, as the post history would still allow you to easily reconstruct an old post.</li>
|
||||
<li><strong>Topic Event Improvements</strong> Topic events such as pinning or locking are now displayed in-line with posts, and plugins can add their own topic events. Previously, topic actions were done in the background, so this increases the transparency of moderation actions.</li>
|
||||
<li><strong>Categories System Refactoring</strong> If your forum only has a few categories then loading all of them is probably not an issue, but increasingly some larger NodeBB forums have hundreds or even thousands of categories, which can cause performance issues. Now the routes that were previously loading all categories can be paginated via a setting in the admin control panel, reducing the load considerably.</li>
|
||||
<li><strong>Filter Tags by Topic</strong> We've added the ability to filter the tags page by category, so users can more easily filter out tags that might not be relevant to what they are looking for.</li>
|
||||
<li><strong>Timeline Design added to Persona Theme</strong> This change to our most popular theme makes it easier to follow conversations, particularly long running ones:
|
||||
<img src="https://nodebb.wpcomstaging.com/wp-content/uploads/2021/08/nodebb-v1-17-0-scheduled-topics-new-moderation-features-and-more.png" alt="NodeBB v1.17.0 - Scheduled Topics, New Moderation Features and More" /></li>
|
||||
<li><strong>Developer Improvements</strong>
|
||||
<ul>
|
||||
<li>Re-mountable routes
|
||||
<ul>
|
||||
<li>It is now possible to (via custom plugin logic) override specific mountpoints in NodeBB. For example, instead of /category (and related pages such as /category/5/category-name, the mount point can be renamed to /kategori for German communities</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Sorted lists available on client side
|
||||
<ul>
|
||||
<li>The "sorted-lists" functionality was primarily an ACP-only library. We now enabled this to be used on the front-end.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Client-side hooks
|
||||
<ul>
|
||||
<li>Now filter, static, and action hooks are available on client-side.</li>
|
||||
<li>A new "hooks" module has been added to v1.17.0, allowing the use of filter, static, and action hooks similar to the server-side. Prior to this, we were limited to <code>$(window).trigger</code> calls, which had limitations, especially for asynchronous tasks. (see <a href="https://github.com/NodeBB/NodeBB/blob/32c20806bc8467953ba354042b870ed8794f8517/public/src/admin/admin.js#L33-L38#L33-L38">https://github.com/NodeBB/NodeBB/blob/32c20806bc8467953ba354042b870ed8794f8517/public/src/admin/admin.js#L33-L38#L33-L38</a> for details)</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Security Fixes
|
||||
<ul>
|
||||
<li>One XSS vulnerability fixed</li>
|
||||
<li>One security best practice implemented, to guard against session fixation attacks</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<p>For a breaking changes, please see:</p>
|
||||
|
||||
<a href="https://community.nodebb.org/topic/15259/1-17-0-breaking-changes">https://community.nodebb.org/topic/15259/1-17-0-breaking-changes</a>
|
||||
|
||||
<p>As usual if you are hosting your own NodeBB forum we highly recommend <a href="https://docs.nodebb.org/configuring/upgrade/">updating to the latest version</a> to take advantage of the latest features and fixes. Always remember to back up your database before proceeding with any upgrade.</p>
|
||||
|
||||
<p>If you encounter any issues, please post them on our community support forum.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-5"/>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<div class="d-flex flex-column flex-md-row gap-5 justify-content-between mb-5 flex-wrap">
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Get Started</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/product" class="link-secondary text-decoration-none">PRODUCT</a></li>
|
||||
<li><a href="/pricing" class="link-secondary text-decoration-none">PRICING</a></li>
|
||||
<li><a href="/services" class="link-secondary text-decoration-none">SERVICES</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Resources</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="https://community.nodebb.org/" class="link-secondary text-decoration-none" target="_blank">COMMUNITY</a></li>
|
||||
<li><a href="https://try.nodebb.org/" class="link-secondary text-decoration-none" target="_blank">DEMO SITE</a></li>
|
||||
<li><a href="https://community.nodebb.org/category/28/answers" class="link-secondary text-decoration-none" target="_blank">ANSWERS</a></li>
|
||||
<li><a href="https://docs.nodebb.org" class="link-secondary text-decoration-none" target="_blank">DOCUMENTATION</a></li>
|
||||
<li><a href="/bounty" class="link-secondary text-decoration-none">BUG BOUNTY</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Company</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/about" class="link-secondary text-decoration-none">ABOUT</a></li>
|
||||
<li><a href="/blog" class="link-secondary text-decoration-none">BLOG</a></li>
|
||||
<li><a href="/contact" class="link-secondary text-decoration-none">CONTACT</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<a href="https://manage.nodebb.org/register" class="btn btn-primary"><i class="fa-solid fa-rocket"></i> Start Free Trial</a>
|
||||
<div class="d-flex gap-3 mt-3 justify-content-between px-2">
|
||||
<a title="NodeBB Github Page" href="https://github.com/nodebb/nodebb" class="link-secondary text-decoration-none"><i class="fab fa-github"></i></a>
|
||||
<a title="NodeBB Twitter Page" href="https://twitter.com/nodebb" class="link-secondary text-decoration-none"><i class="fab fa-twitter"></i></a>
|
||||
<a title="NodeBB Mastodon Page" href="https://fosstodon.org/@nodebb" class="link-secondary text-decoration-none"><i class="fa-brands fa-mastodon"></i></a>
|
||||
<a title="NodeBB Facebook Page" href="https://www.facebook.com/NodeBB" class="link-secondary text-decoration-none"><i class="fab fa-facebook"></i></a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap justify-content-between gap-5 small">
|
||||
<span class="text-secondary text-nowrap">©2025 NodeBB, Inc. — Made in Canada with <i class="fa-solid fa-heart text-danger"></i>.</span>
|
||||
<div class="d-flex gap-3">
|
||||
<a href="/tos" class="link-secondary text-nowrap text-decoration-none">Terms of Service</a>
|
||||
<a href="/privacy" class="link-secondary text-nowrap text-decoration-none">Privacy Policy</a>
|
||||
<a href="/gdpr" class="link-secondary text-nowrap text-decoration-none">GDPR</a>
|
||||
<a href="/dmca" class="link-secondary text-nowrap text-decoration-none">DMCA</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap 5 JS Bundle (includes Popper) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
376
blog/optimizing-benchpress.html
Normal file
|
@ -0,0 +1,376 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Optimizing Benchpress - NodeBB - Modern Community Forum Software</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Blog of NodeBB Forum Software - The Modern Discussion Platform">
|
||||
<meta name="author" content="NodeBB Inc.">
|
||||
<meta name="keywords" content="nodebb, node.js, forum, discussion, community, software, hosting, blog">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/icons/32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/icons/16x16.png">
|
||||
<link rel="shortcut icon" href="/images/icons/favicon.ico">
|
||||
|
||||
<!-- Google Fonts: Inter & Poppins -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Poppins:wght@400;600&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<!-- our css-->
|
||||
<link href="/css/style.css" rel="stylesheet">
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-V0P62EB8Q6"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-V0P62EB8Q6');
|
||||
</script>
|
||||
</head>
|
||||
<body class="p-0 py-5 p-lg-5">
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar navbar-expand-lg bg-body fixed-top shadow-sm">
|
||||
<div class="container-lg">
|
||||
<a class="navbar-brand py-2" href="/">
|
||||
<img src="/images/brand/nodebb-logo.svg" style="height: 36px; width: auto;" alt="NodeBB Logo" />
|
||||
</a>
|
||||
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMenu" aria-controls="navbarMenu" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse justify-content-end" id="navbarMenu">
|
||||
<ul class="nav nav-underline gap-4 flex-column flex-lg-row align-items-end align-items-lg-center mt-4 mt-lg-0">
|
||||
<li class="nav-item">
|
||||
<a href="/" class="nav-link text-reset fw-semibold">HOME</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/product" class="nav-link text-reset fw-semibold">PRODUCT</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/pricing" class="nav-link text-reset fw-semibold">PRICING</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/services" class="nav-link text-reset fw-semibold">SERVICES</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" target="_blank" role="button" data-bs-toggle="dropdown" aria-expanded="false" class="nav-link dropdown-toggle text-reset fw-semibold">RESOURCES</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end border-0 shadow p-1">
|
||||
<li><a href="https://community.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Community</a></li>
|
||||
<li><a href="https://try.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Demo Site</a></li>
|
||||
<li><a href="https://community.nodebb.org/category/28/answers" class="dropdown-item rounded-1" target="_blank">Answers</a></li>
|
||||
<li><a href="https://docs.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Documentation</a></li>
|
||||
<li><a href="/bounty" class="dropdown-item rounded-1">Bug Bounty</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/contact" class="btn btn-warning rounded-pill">Contact Us</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="https://manage.nodebb.org/register" class="btn btn-primary"><i class="fa-solid fa-rocket"></i> Start Free Trial</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="container-lg mt-5">
|
||||
<!-- Home -->
|
||||
<div id="home-tab-pane">
|
||||
<div class="row pt-2 pt-lg-5">
|
||||
<div class="col-12 d-flex flex-column gap-4">
|
||||
<h1 class="display-1 fw-bold fs-1">
|
||||
Optimizing Benchpress
|
||||
</h1>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- blog post -->
|
||||
<div class="py-5">
|
||||
<div class="d-flex gap-2 align-items-center mb-4">
|
||||
<a href="https://community.nodebb.org/user/pitaj"><img class="rounded-circle" width="32" src="https://community.nodebb.org/assets/uploads/profile/uid-3076/3076-profileavatar-1639631260637.jpeg"></a> <a href="https://community.nodebb.org/user/pitaj" class="fw-semibold">Peter Jaszkowiak</a> <span class="text-secondary">11/23/2020, 6:43:38 PM</span>
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="kg-card-markdown"><h1 id="optimizingbenchpress">Optimizing Benchpress</h1>
|
||||
<p>Recently, I saw the release of <a href="https://github.com/Geal/nom/">nom v6</a> and decided I wanted to try it out, and see if I could speed up my hobby JS template compiler, <a href="https://github.com/benchpressjs/benchpressjs">BenchpressJS</a>.</p>
|
||||
<h2 id="background">Background</h2>
|
||||
<p>Benchpress is a template compiler and tiny runtime which is focused on two things:</p>
|
||||
<ul>
|
||||
<li>Backwards compatibility with <a href="https://github.com/benchpressjs/benchpressjs/tree/templates.js-legacy">templates.js</a> syntax</li>
|
||||
<li>Runtime speed</li>
|
||||
<li>(As of 2018) Compilation speed</li>
|
||||
</ul>
|
||||
<p>A short history:</p>
|
||||
<ul>
|
||||
<li>2014: templates.js is created as a light library for use in NodeBB.<br>
|
||||
It used Regular Expressions to fill in template data at runtime, allowing for an unconventional syntax. No precompilation is necessary or possible.</li>
|
||||
<li>2017: Benchpress is created as a backwards-compatible replacement for templates.js<br>
|
||||
It provides much faster runtime performance by compiling templates into Javascript code.</li>
|
||||
<li>2018: A compiler rewrite is undertaken utilizing Rust, as the JS compiler (based on Regexp) is quite slow.<br>
|
||||
This version of the compiler, named <a href="https://github.com/benchpressjs/benchpress-rs"><strong>benchpress-rs</strong></a>, uses <a href="https://neon-bindings.com/">Neon</a> to interface with Node, and is about 30x faster than the JS version.<br>
|
||||
The JS compiler continues to ship alongside, since pre-built binaries are not available for all platforms.</li>
|
||||
<li>Nov 7, 2020: Realizing WASM module support is available on all supported version of Node, the JS compiler is removed, and the Rust compiler is shipped solo as a WASM module. Having a single codebase opens up the door to adding more features, but benchpress-rs is pretty hacky and difficult to work on.</li>
|
||||
<li>Nov 15, 2020: Another compiler rewrite is finished, using the <strong>nom</strong> parser combinator library.<br>
|
||||
This rewrite is optimized to be 4.4x as fast as the previous version, on top of being far more maintainable and extensible.<br>
|
||||
<em>I'd say a week is pretty good, all things considered</em></li>
|
||||
</ul>
|
||||
<h2 id="thedrop">The Drop</h2>
|
||||
<p>Much of the time implementing the rewrite was getting my new version to pass the <a href="https://github.com/benchpressjs/benchpressjs/tree/master/tests/templates/source">quite extensive suite of integration tests</a> (especially getting Spans working), but that's not what I'm here to talk about. I'm here to talk about optimizing it after all of that, because I have a shameful secret:</p>
|
||||
<blockquote>
|
||||
<p>It was <em>slower</em>.</p>
|
||||
</blockquote>
|
||||
<p>Yep, after all of that work, the end result was actually slower than the previous version. Let's see the benchmarks.</p>
|
||||
<h4 id="benchmarksetup">Benchmark Setup</h4>
|
||||
<p>I set up benchmarks both in Rust and Node. Both are based on two large template files: <code>categories.tpl</code> and <code>topic.tpl</code>, which are from some NodeBB at some point I think.</p>
|
||||
<p>Essentially, each benchmark compiles these repeatedly. Here's what I saw:</p>
|
||||
<p><strong>Before</strong></p>
|
||||
<pre><code class="language-sh">$ cargo +nightly bench
|
||||
running 2 tests
|
||||
test bench_compile_categories ... bench: 329,287 ns/iter (+/- 120,222)
|
||||
test bench_compile_topic ... bench: 4,402,767 ns/iter (+/- 63,216) test result: ok. 0 passed; 0 failed; 0 ignored; 2 measured; 0 filtered out $ grunt bench 2>/dev/null
|
||||
Running "benchmark" task
|
||||
compilation x 74.70 ops/sec ±0.32% (167 runs sampled) Done.
|
||||
</code></pre>
|
||||
<p><strong>After</strong></p>
|
||||
<pre><code class="language-sh">$ cargo +nightly bench
|
||||
running 2 tests
|
||||
test bench_compile_categories ... bench: 2,498,374 ns/iter (+/- 11,032)
|
||||
test bench_compile_topic ... bench: 15,065,914 ns/iter (+/- 51,794) test result: ok. 0 passed; 0 failed; 0 ignored; 2 measured; 0 filtered out $ grunt bench 2>/dev/null
|
||||
Running "benchmark" task
|
||||
compilation x 33.88 ops/sec ±0.26% (175 runs sampled) Done.
|
||||
</code></pre>
|
||||
<p>Woops! That's more than twice as slow. And even worse, the vast majority of that time is spent in Rust.</p>
|
||||
<h4 id="flamegraphing">Flamegraphing</h4>
|
||||
<p>One of the best tools, in my opinion, for finding optimization opportunities is the flamegraph. This neat little figure will show you where the time is spent in your program. So, let's make one with the very handy <a href="https://github.com/flamegraph-rs/flamegraph">cargo flamegraph tool</a>:</p>
|
||||
<pre><code class="language-sh">$ cargo +nightly flamegraph --bin bench # bench refers to a binary based on the benchmarks run before
|
||||
</code></pre>
|
||||
<p><a href="https://blog.nodebb.org/content/images/2020/11/flamegraph_before.svg"><img src="https://blog.nodebb.org/content/images/2020/11/flamegraph_before.svg" alt="Flamegraph, click to open in your browser"></a></p>
|
||||
<p>Isn't it beautiful? Alright let's dig in. Here's how to read the graph, from the flamegraph README:</p>
|
||||
<blockquote>
|
||||
<p>The <strong>y-axis</strong> shows the stack depth number. When looking at a flamegraph, the main function of your program will be closer to the bottom, and the called functions will be stacked on top, with the functions that they call stacked on top of them, etc...</p>
|
||||
<p>The <strong>x-axis</strong> spans all of the samples. It does <em>not</em> show the passing of time from left to right. The left to right ordering has no meaning.</p>
|
||||
<p>The <strong>width</strong> of each box shows the total time that that function is on the CPU or is part of the call stack. If a function's box is wider than others, that means that it consumes more CPU per execution than other functions, or that it is called more than other functions.</p>
|
||||
<p>The <strong>color</strong> of each box isn't significant, and is chosen at random.</p>
|
||||
</blockquote>
|
||||
<p>First, let's ignore anything not above <code>compiler::compile</code>, since that's really what we care about. It's clear that the majority of the program is spent in <code>compiler::parse::tokens</code>, so let's zoom in on that part by clicking on it.</p>
|
||||
<p>Now we see something like this:</p>
|
||||
<p><a href="https://nodebb.org/blog/images/optimizing-benchpress-2.png"><img src="https://nodebb.org/blog/images/optimizing-benchpress-2.png" alt="Click to open full flamegraph"></a></p>
|
||||
<p>Hmmm. Nothing sticks out to me right off the bat. All of the token parsers seem to be taking a portion of the time that matches what I'd expect.</p>
|
||||
<p>This had me stumped for a while. Then I figured out what I was missing.</p>
|
||||
<h3 id="optimization1tokenrecognition">Optimization #1: Token recognition</h3>
|
||||
<p>There are a couple of clues in the flamegraph that give it away:</p>
|
||||
<ol>
|
||||
<li><code>interp_escaped</code> and <code>interp_raw</code> share a similar amount of time. But we should be hitting <code>interp_escaped</code> way more than <code>interp_raw</code>, because escaped interpolation tokens show up far more often in the benchmark templates.</li>
|
||||
<li>There are a lot of hits on <code>new_else</code> and <code>new_end</code>, but those don't show up in the templates <em>at all</em>.</li>
|
||||
<li>We're hitting <code>from_error</code> and <code>into_result</code> a lot, which implies the parsers are erroring a lot.</li>
|
||||
</ol>
|
||||
<p>It turns out, the way token recognition works is extremely expensive. Here's the code:</p>
|
||||
<pre><code class="language-rust">let mut res = Vec::new();
|
||||
let mut index = 0; while index < input.len() { match sep.parse(input.slice(index..)) { Err(nom::Err::Error(_)) => { // do-while while { index += 1; !input.is_char_boundary(index) } {} } Err(e) => return Err(e), Ok((rest, mat)) => { // if this match was escaped, skip it if input.slice(..index).ends_with('\\') { let before_escape = input.slice(..(index - 1)); if before_escape.len() > 0 { res.push(f(before_escape)); } input = input.slice(index..); index = nom::Offset::offset(&input, &rest); continue; } if rest == input { return Err(nom::Err::Error(E::from_error_kind( rest, nom::error::ErrorKind::SeparatedList, ))); } if index > 0 { res.push(f(input.slice(..index))); } res.push(mat); input = rest; index = 0; } }
|
||||
} if index > 0 { res.push(f(input.slice(..index)));
|
||||
} Ok((input.slice(input.len()..), res))
|
||||
</code></pre>
|
||||
<p>Do you see it? For every byte in the input, we're running the full parsing suite, checking it against 10 parsers! This works, but it's quite naive. For context, Benchpress tokens come in three shapes:</p>
|
||||
<ul>
|
||||
<li>Interpolation: <code>{thing_escaped}</code>, <code>{{raw_stuff}}</code></li>
|
||||
<li>Modern Control Flow: <code>{{{ if cond }}}</code>, <code>{{{ each arr }}}</code>, <code>{{{ else }}}</code>, <code>{{{ end }}}</code></li>
|
||||
<li>Legacy Control Flow: <code><!-- IF cond --></code>, <code><!-- BEGIN arr --></code>, <code><!-- ELSE --></code>, <code><!-- END(IF) stuff --></code></li>
|
||||
</ul>
|
||||
<p>Which means that we don't need to check these at every point in the input. We only need to run the parsers when we hit an opening curly brace <code>{</code> or an opening comment-arrow <code><!--</code>. It just so happens that there's a nice rust library made exactly for the purpose of searching for multiple patterns in text: <a href="https://github.com/BurntSushi/aho-corasick">aho-corasick</a>, which also backs the Rust regex crate.</p>
|
||||
<p>Let's refactor the code to use that. I also want the refactor to handle escaping tokens here as well. aho-corasick makes this easy: we just need to define the patterns we want to look for, and tell it to use the <code>LeftmostFirst</code> mode, so it will match the escaped patterns we define first before it hits the unescaped start patterns:</p>
|
||||
<pre><code class="language-rust">static PATTERNS: &[&str] = &["\\{{{", "\\{{", "\\{", "\\<!--", "{", "<!--"]; use aho_corasick::{ AhoCorasick, AhoCorasickBuilder, MatchKind,
|
||||
};
|
||||
lazy_static::lazy_static! { static ref TOKEN_START: AhoCorasick = AhoCorasickBuilder::new() .auto_configure(PATTERNS) .match_kind(MatchKind::LeftmostFirst) .build(PATTERNS);
|
||||
}
|
||||
</code></pre>
|
||||
<p>Now we can use <code>TOKEN_START</code> in our hot loop to skip over portions of text with no tokens:</p>
|
||||
<pre><code class="language-rust">pub fn tokens(mut input: Span) -> IResult<Span, Vec<Token<'_>>> { let mut tokens = vec![]; let mut index = 0; while index < input.len() { // skip to the next `{` or `<!--` if let Some(i) = TOKEN_START.find(input.slice(index..).fragment()) { // If this is an escaped opener, skip it if i.pattern() <= 3 { let start = index + i.start(); let length = i.end() - i.start(); // Add text before the escaper character let before_escape = input.slice(..start); if before_escape.len() > 0 { tokens.push(Token::Text(before_escape)); } // Advance to after the escaper character input = input.slice((start + 1)..); // Step to after the escaped sequence index = length - 1; } else { index += i.start(); } } else { // no tokens found, break out index = input.len(); break; } match token(input.slice(index..)) { // Not a match, step to the next character Err(nom::Err::Error(_)) => { // do-while while { index += 1; !input.is_char_boundary(index) } {} } Ok((rest, tok)) => { // Token returned what it was sent, this shouldn't happen if rest == input { return Err(nom::Err::Error(nom::error::Error::from_error_kind( rest, nom::error::ErrorKind::SeparatedList, ))); } // Add test before the token if index > 0 { tokens.push(Token::Text(input.slice(..index))); } // Add token tokens.push(tok); // Advance to after the token input = rest; index = 0; } // Pass through other errors Err(e) => return Err(e), } } if index > 0 { tokens.push(Token::Text(input.slice(..index))); } Ok((input.slice(input.len()..), tokens))
|
||||
}
|
||||
</code></pre>
|
||||
<p>This looks cleaner and requires no backtracking for escaped tokens. What did it gain us in performance?</p>
|
||||
<h4 id="benchmarksround2">Benchmarks Round 2</h4>
|
||||
<pre><code class="language-sh">$ cargo +nightly bench
|
||||
running 2 tests
|
||||
test bench_compile_categories ... bench: 241,765 ns/iter (+/- 4,493)
|
||||
test bench_compile_topic ... bench: 3,969,554 ns/iter (+/- 43,614) test result: ok. 0 passed; 0 failed; 0 ignored; 2 measured; 0 filtered out $ grunt bench 2>/dev/null
|
||||
Running "benchmark" task
|
||||
compilation x 103 ops/sec ±0.38% (176 runs sampled) Done.
|
||||
</code></pre>
|
||||
<p>Nice! That's even slightly faster than benchpress-rs, so we've made a lot of progress. But there's still room for improvement.</p>
|
||||
<p>Let's see another flamegraph:</p>
|
||||
<p><a href="https://nodebb.org/blog/images/optimizing-benchpress-3.png"><img src="https://nodebb.org/blog/images/optimizing-benchpress-3.png" alt="Click to open full flamegraph"></a></p>
|
||||
<p>Look at that, tokens parsing is now a small portion of the compilation. This puts a big target on the next step: replacing the prefixer with additions to the parser.</p>
|
||||
<h3 id="optimization2prefixerforexternalkeywords">Optimization #2: Prefixer for external keywords</h3>
|
||||
<p>The "prefixer" is what I call a simple backwards-compatibility layer which runs before the parsing step. The prefixer is based on Regular Expressions, which results in it being quite slow. It does a few things:</p>
|
||||
<ul>
|
||||
<li>Detects keywords like <code>@value</code>, <code>@key</code>, <code>@index</code> outside interpolation tokens, and wraps them in curly braces: <code>{@value}</code></li>
|
||||
<li>Detects legacy loop helpers with no arguments like <code>{function.print_element}</code>, and changes them to be called with the current element as the first argument: <code>{function.print_element, @value}</code></li>
|
||||
<li>Detects legacy IF-helpers like <code><!-- IF function.works, stuff --></code> and adds the top-level context as the first argument: <code><!-- IF function.works, @root, stuff --></code>, but <em>only</em> if IF conditions</li>
|
||||
<li>Detects nested legacy <code><!-- BEGIN arr --></code> blocks and duplicates them as <code><!-- IF ./arr --><!-- BEGIN ./arr -->...<!-- ELSE --><!-- BEGIN arr -->...</code><br>
|
||||
The legacy syntax is ambiguous: templates.js would work if <code>arr</code> was a top-level value or if <code>arr</code> was a property of the current element. Because of this, we have to emit code for both cases.</li>
|
||||
</ul>
|
||||
<p>Let's move the first case into our parser instead of relying on Regex. Luckily, we just made our jobs a lot easier with that refactor of the token recognition. All we need to do is add those keywords to our aho-corasick patterns:</p>
|
||||
<pre><code class="language-rust">static PATTERNS: &[&str] = &["\\{{{", "\\{{", "\\{", "\\<!--", "{", "<!--", "@key", "@value", "@index"];
|
||||
</code></pre>
|
||||
<p>And then handle those cases in the tokens parser, parsing them as an expression and adding that to our collection of tokens:</p>
|
||||
<pre><code class="language-rust">// If this is an escaped opener, skip it
|
||||
if matches!(i.pattern(), 0..=3) { let start = index + i.start(); let length = i.end() - i.start(); // Add text before the escaper character if start > 0 { tokens.push(Token::Text(input.slice(..start))); } // Advance to after the escaper character input = input.slice((start + 1)..); // Step to after the escaped sequence index = length - 1;
|
||||
// If this is an opener, step to it
|
||||
} else if matches!(i.pattern(), 4..=5) { index += i.start();
|
||||
// If this is `@key`, `@value`, `@index`
|
||||
} else { let start = index + i.start(); let end = index + i.end(); let span = input.slice(start..end); let (_, expr) = expression(span)?; // Add text before the token if start > 0 { tokens.push(Token::Text(input.slice(..start))); } // Add token tokens.push(Token::InterpEscaped { span, expr }); // Advance to after the token input = input.slice(end..); index = 0;
|
||||
}
|
||||
</code></pre>
|
||||
<p>Let's remove the relevant parts of the prefixer and see what this bought us.</p>
|
||||
<h4 id="benchmarksround3">Benchmarks Round 3</h4>
|
||||
<pre><code class="language-sh">$ cargo +nightly bench
|
||||
running 2 tests
|
||||
test bench_compile_categories ... bench: 194,683 ns/iter (+/- 5,158)
|
||||
test bench_compile_topic ... bench: 2,109,842 ns/iter (+/- 26,943) test result: ok. 0 passed; 0 failed; 0 ignored; 2 measured; 0 filtered out $ grunt bench 2>/dev/null
|
||||
Running "benchmark" task
|
||||
compilation x 172 ops/sec ±0.26% (176 runs sampled) Done.
|
||||
</code></pre>
|
||||
<p>Another nice boost to our compilation speed. Onward!</p>
|
||||
<h3 id="optimization3legacyloophelpers">Optimization #3: Legacy loop helpers</h3>
|
||||
<p>Our second prefixer case is quite simple: when we encounter a legacy helper called with no arguments, we need to implicitly call it with <code>@value</code>. This we can implement in the expression parser.</p>
|
||||
<p>Before, we just provided an empty argument list to a legacy helper:</p>
|
||||
<pre><code class="language-rust">fn legacy_helper(input: Span) -> IResult<Span, Expression<'_>> { map( consumed(pair( preceded(tag("function."), identifier), opt(preceded( ws(tag(",")), separated_list0(ws(tag(",")), expression), )), )), |(span, (name, args))| Expression::LegacyHelper { span, name, args: args.unwrap_or_default(), // Provides an empty Vec }, )(input)
|
||||
}
|
||||
</code></pre>
|
||||
<p>But now, we want to give it <code>@value</code> in those cases, which we can achieve with <code>Option::unwrap_or_else</code>:</p>
|
||||
<pre><code class="language-rust"> args: args.unwrap_or_else(|| { // Handle legacy helpers without args being implicitly passed `@value` vec![Expression::Path(vec![PathPart::Part(Span::new_extra( "@value", input.extra, ))])] }),
|
||||
</code></pre>
|
||||
<p>Let's see the benchmarks with this removed from the prefixer.</p>
|
||||
<h4 id="benchmarksround4">Benchmarks Round 4</h4>
|
||||
<pre><code class="language-sh">$ cargo +nightly bench
|
||||
running 2 tests
|
||||
test bench_compile_categories ... bench: 187,462 ns/iter (+/- 2,519)
|
||||
test bench_compile_topic ... bench: 2,030,519 ns/iter (+/- 19,016) test result: ok. 0 passed; 0 failed; 0 ignored; 2 measured; 0 filtered out $ grunt bench 2>/dev/null
|
||||
Running "benchmark" task
|
||||
compilation x 180 ops/sec ±0.32% (181 runs sampled) Done.
|
||||
</code></pre>
|
||||
<p>Not a huge bump, likely because this doesn't show up a ton in templates, but an improvement nonetheless.</p>
|
||||
<h3 id="optimization4legacyifhelpers">Optimization #4: Legacy IF helpers</h3>
|
||||
<p>Next step in my quest to dismantle the prefixer is to handle <code><!-- IF function.foo, bar --></code>. Essentially, I need to check if the conditional is a legacy helper expression, and if so, prepend the arguments with <code>@root</code>. We can implement this in the token parser.</p>
|
||||
<p>Before, we just provided the unmodified expression:</p>
|
||||
<pre><code class="language-rust">fn legacy_if(input: Span) -> IResult<Span, Token<'_>> { map( consumed(delimited( pair(tag("<!--"), ws(tag("IF"))), ws(consumed(expression)), tag("-->"), )), |(span, (subject_raw, subject))| Token::LegacyIf { span, subject_raw, subject, // Unmodified expression }, )(input)
|
||||
}
|
||||
</code></pre>
|
||||
<p>Rust's extensive pattern-matching makes it easy to check for a <code>LegacyHelper</code> expression, and modify it accordingly:</p>
|
||||
<pre><code class="language-rust"> subject: { // Handle legacy IF helpers being passed @root as implicit first argument if let Expression::LegacyHelper { span, name, mut args } = subject { args.insert(0, Expression::Path(vec![PathPart::Part(Span::new_extra( "@root", input.extra, ))])); Expression::LegacyHelper { span, name, args } } else { subject } },
|
||||
</code></pre>
|
||||
<p>This fix step was implemented with a <a href="https://github.com/benchpressjs/benchpressjs/blob/a263019ac54e18d393e405306049f5be68e30328/benchpress_sys/src/pre_fixer.rs#L49-L57">pretty nasty Regex</a>, so I expected a pretty substantial performance improvement.</p>
|
||||
<h4 id="benchmarksround5">Benchmarks Round 5</h4>
|
||||
<pre><code class="language-sh">$ cargo +nightly bench
|
||||
running 2 tests
|
||||
test bench_compile_categories ... bench: 196,249 ns/iter (+/- 13,057)
|
||||
test bench_compile_topic ... bench: 2,028,033 ns/iter (+/- 16,110) test result: ok. 0 passed; 0 failed; 0 ignored; 2 measured; 0 filtered out $ grunt bench 2>/dev/null
|
||||
Running "benchmark" task
|
||||
compilation x 178 ops/sec ±0.37% (178 runs sampled) Done.
|
||||
</code></pre>
|
||||
<p>But really, performance was about the same, because this pattern actually never shows up in the benchmark templates. That's okay though, let's move on.</p>
|
||||
<h3 id="optimization5ambiguousinnerbegin">Optimization #5: Ambiguous inner BEGIN</h3>
|
||||
<p>With those taken care of, the only thing left is handling this:</p>
|
||||
<pre><code class="language-html"><!-- BEGIN people --> person {people.name} has the following pets: <!-- BEGIN pets --> - {pets.name} <!-- END pets -->
|
||||
<!-- END people -->
|
||||
</code></pre>
|
||||
<p>The inner <code>pets</code> loop is ambiguous. There's no way for the compiler to know that it should refer to the pets for each element of people. It could also refer to the top-level <code>pets</code> value. So we must emit code for both cases, by transforming it to this:</p>
|
||||
<pre><code class="language-html"><!-- BEGIN people --> person {people.name} has the following pets: <!-- IF ./pets --><!-- BEGIN ./pets --> - {pets.name} <!-- END pets --><!-- ELSE --><!-- BEGIN pets --> - {pets.name} <!-- END pets --><!-- ENDIF ./pets -->
|
||||
<!-- END people -->
|
||||
</code></pre>
|
||||
<p>This will be the most difficult to move into code. I won't bore you with the details, but here's a quick run-down. When this condition is detected, we interpret the tokens ahead both as if it was a relative path and as if it was an absolute path, then wrap those in a conditional. Otherwise, we emit it as we would normally.</p>
|
||||
<p>Rust's closures help with this immensely, allowing me to deduplicate the code into a closure, which I call depending on the case.</p>
|
||||
<pre><code class="language-rust">// create an iteration intruction
|
||||
Token::LegacyBegin { span, subject, subject_raw,
|
||||
} => { let normal = |input: &mut I, subject| { let mut body = vec![]; let mut alt = vec![]; let subject = resolve_expression_paths(base, subject); let base: PathBuf = if let Expression::Path(base) = &subject { let mut base = base.clone(); if let Some(last) = base.last_mut() { last.with_depth(depth) } base } else { base.to_vec() }; match tree(depth + 1, &base, input, &mut body)? { Some(Token::LegacyElse { .. }) | Some(Token::Else { .. }) => { // consume the end after the else match tree(depth, &base, input, &mut alt)? { Some(Token::LegacyEnd { .. }) | Some(Token::End { .. }) => {} _ => return Err(TreeError), } } Some(Token::LegacyEnd { .. }) | Some(Token::End { .. }) => {} _ => return Err(TreeError), } Ok(Instruction::Iter { depth, subject_raw, subject, body, alt, }) }; // Handle legacy `<!-- BEGIN stuff -->` working for top-level `stuff` and implicitly `./stuff` match &subject { Expression::Path(path) if depth > 0 && path.first().map_or(false, |s| { // Not a relative path or keyword !s.inner().starts_with(&['.', '@'] as &[char]) }) => { // Path is absolute, so create a branch for both `./subject` and `subject` let mut relative_path = vec![PathPart::Part(Span::new_extra("./", span.extra))]; relative_path.extend_from_slice(path); let relative_subject = Expression::Path(relative_path); Instruction::If { subject: resolve_expression_paths(base, relative_subject.clone()), body: vec![normal(&mut input.clone(), relative_subject)?], alt: vec![normal(input, subject)?], } } _ => normal(input, subject)?, }
|
||||
}
|
||||
</code></pre>
|
||||
<p>Yes! The prefixer is gone. Time for a final set of benchmarks.</p>
|
||||
<h4 id="finalbenchmarks">Final Benchmarks</h4>
|
||||
<pre><code class="language-sh">$ cargo +nightly bench
|
||||
running 2 tests
|
||||
test bench_compile_categories ... bench: 111,440 ns/iter (+/- 4,479)
|
||||
test bench_compile_topic ... bench: 944,981 ns/iter (+/- 6,062) test result: ok. 0 passed; 0 failed; 0 ignored; 2 measured; 0 filtered out $ grunt bench 2>/dev/null
|
||||
Running "benchmark" task
|
||||
compilation x 327 ops/sec ±0.53% (180 runs sampled) Done.
|
||||
</code></pre>
|
||||
<p>Check that out! We're 10x faster than we started, and 4.4x faster than benchpress-rs. <em>Fantastic</em>.</p>
|
||||
<p>Let's take a final look at the flamegraph:</p>
|
||||
<p><a href="https://nodebb.org/blog/images/optimizing-benchpress-4.png"><img src="https://nodebb.org/blog/images/optimizing-benchpress-4.png" alt="Click to open full flamegraph"></a></p>
|
||||
<p>Now we can see that our program spends about equal time parsing and generating output.</p>
|
||||
<h2 id="finalremarks">Final Remarks</h2>
|
||||
<p>After all of that, I was quite proud of myself. Ditching the prefixer also has a huge side-benefit: true source location information. Having that information allows me to emit some helpful Rust-inspired warnings like this:</p>
|
||||
<pre><code class="language-text">[benchpress] warning: keyword outside an interpolation token is deprecated --> tests/templates/source/loop-tokens-conditional.tpl:3:39 | 3 | <span class="label label-primary">@key</span> | ^^^^ help: wrap this in curly braces: `{@key}` | note: This will become an error in the v3.0.0
|
||||
</code></pre>
|
||||
<p>Overall, I'm very happy with <strong>nom</strong>. It made the rewrite much easier than it would have been, had I tried to custom write another parser. It made the resulting code much easier to read and more maintainable. And without its flexibility, I would never have been able to refactor out the prefixer.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-5"/>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<div class="d-flex flex-column flex-md-row gap-5 justify-content-between mb-5 flex-wrap">
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Get Started</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/product" class="link-secondary text-decoration-none">PRODUCT</a></li>
|
||||
<li><a href="/pricing" class="link-secondary text-decoration-none">PRICING</a></li>
|
||||
<li><a href="/services" class="link-secondary text-decoration-none">SERVICES</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Resources</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="https://community.nodebb.org/" class="link-secondary text-decoration-none" target="_blank">COMMUNITY</a></li>
|
||||
<li><a href="https://try.nodebb.org/" class="link-secondary text-decoration-none" target="_blank">DEMO SITE</a></li>
|
||||
<li><a href="https://community.nodebb.org/category/28/answers" class="link-secondary text-decoration-none" target="_blank">ANSWERS</a></li>
|
||||
<li><a href="https://docs.nodebb.org" class="link-secondary text-decoration-none" target="_blank">DOCUMENTATION</a></li>
|
||||
<li><a href="/bounty" class="link-secondary text-decoration-none">BUG BOUNTY</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Company</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/about" class="link-secondary text-decoration-none">ABOUT</a></li>
|
||||
<li><a href="/blog" class="link-secondary text-decoration-none">BLOG</a></li>
|
||||
<li><a href="/contact" class="link-secondary text-decoration-none">CONTACT</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<a href="https://manage.nodebb.org/register" class="btn btn-primary"><i class="fa-solid fa-rocket"></i> Start Free Trial</a>
|
||||
<div class="d-flex gap-3 mt-3 justify-content-between px-2">
|
||||
<a title="NodeBB Github Page" href="https://github.com/nodebb/nodebb" class="link-secondary text-decoration-none"><i class="fab fa-github"></i></a>
|
||||
<a title="NodeBB Twitter Page" href="https://twitter.com/nodebb" class="link-secondary text-decoration-none"><i class="fab fa-twitter"></i></a>
|
||||
<a title="NodeBB Mastodon Page" href="https://fosstodon.org/@nodebb" class="link-secondary text-decoration-none"><i class="fa-brands fa-mastodon"></i></a>
|
||||
<a title="NodeBB Facebook Page" href="https://www.facebook.com/NodeBB" class="link-secondary text-decoration-none"><i class="fab fa-facebook"></i></a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap justify-content-between gap-5 small">
|
||||
<span class="text-secondary text-nowrap">©2025 NodeBB, Inc. — Made in Canada with <i class="fa-solid fa-heart text-danger"></i>.</span>
|
||||
<div class="d-flex gap-3">
|
||||
<a href="/tos" class="link-secondary text-nowrap text-decoration-none">Terms of Service</a>
|
||||
<a href="/privacy" class="link-secondary text-nowrap text-decoration-none">Privacy Policy</a>
|
||||
<a href="/gdpr" class="link-secondary text-nowrap text-decoration-none">GDPR</a>
|
||||
<a href="/dmca" class="link-secondary text-nowrap text-decoration-none">DMCA</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap 5 JS Bundle (includes Popper) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
202
blog/the-api-continues-to-evolve.html
Normal file
|
@ -0,0 +1,202 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>The API continues to evolve... - NodeBB - Modern Community Forum Software</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Blog of NodeBB Forum Software - The Modern Discussion Platform">
|
||||
<meta name="author" content="NodeBB Inc.">
|
||||
<meta name="keywords" content="nodebb, node.js, forum, discussion, community, software, hosting, blog">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/icons/32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/icons/16x16.png">
|
||||
<link rel="shortcut icon" href="/images/icons/favicon.ico">
|
||||
|
||||
<!-- Google Fonts: Inter & Poppins -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Poppins:wght@400;600&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<!-- our css-->
|
||||
<link href="/css/style.css" rel="stylesheet">
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-V0P62EB8Q6"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-V0P62EB8Q6');
|
||||
</script>
|
||||
</head>
|
||||
<body class="p-0 py-5 p-lg-5">
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar navbar-expand-lg bg-body fixed-top shadow-sm">
|
||||
<div class="container-lg">
|
||||
<a class="navbar-brand py-2" href="/">
|
||||
<img src="/images/brand/nodebb-logo.svg" style="height: 36px; width: auto;" alt="NodeBB Logo" />
|
||||
</a>
|
||||
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMenu" aria-controls="navbarMenu" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse justify-content-end" id="navbarMenu">
|
||||
<ul class="nav nav-underline gap-4 flex-column flex-lg-row align-items-end align-items-lg-center mt-4 mt-lg-0">
|
||||
<li class="nav-item">
|
||||
<a href="/" class="nav-link text-reset fw-semibold">HOME</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/product" class="nav-link text-reset fw-semibold">PRODUCT</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/pricing" class="nav-link text-reset fw-semibold">PRICING</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/services" class="nav-link text-reset fw-semibold">SERVICES</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" target="_blank" role="button" data-bs-toggle="dropdown" aria-expanded="false" class="nav-link dropdown-toggle text-reset fw-semibold">RESOURCES</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end border-0 shadow p-1">
|
||||
<li><a href="https://community.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Community</a></li>
|
||||
<li><a href="https://try.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Demo Site</a></li>
|
||||
<li><a href="https://community.nodebb.org/category/28/answers" class="dropdown-item rounded-1" target="_blank">Answers</a></li>
|
||||
<li><a href="https://docs.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Documentation</a></li>
|
||||
<li><a href="/bounty" class="dropdown-item rounded-1">Bug Bounty</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/contact" class="btn btn-warning rounded-pill">Contact Us</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="https://manage.nodebb.org/register" class="btn btn-primary"><i class="fa-solid fa-rocket"></i> Start Free Trial</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="container-lg mt-5">
|
||||
<!-- Home -->
|
||||
<div id="home-tab-pane">
|
||||
<div class="row pt-2 pt-lg-5">
|
||||
<div class="col-12 d-flex flex-column gap-4">
|
||||
<h1 class="display-1 fw-bold fs-1">
|
||||
The API continues to evolve...
|
||||
</h1>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- blog post -->
|
||||
<div class="py-5">
|
||||
<div class="d-flex gap-2 align-items-center mb-4">
|
||||
<a href="https://community.nodebb.org/user/julian"><img class="rounded-circle" width="32" src="https://community.nodebb.org/assets/uploads/profile/uid-2/2-profileavatar-1738544541106.jpeg"></a> <a href="https://community.nodebb.org/user/julian" class="fw-semibold">Julian Lam</a> <span class="text-secondary">12/7/2020, 12:36:00 PM</span>
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="kg-card-markdown"><p>A couple months back as part of our <a href="https://blog.nodebb.org/api-continues-to-evolve/link">Roadmap to v2</a>, I made the claim that one of the large features in that release would be the merging of the <a href="//github.com/NodeBB/nodebb-plugin-write-api">Write API plugin</a> into core. The majority of the exploratory work had been completed in a development branch reserved for v2-only changes, but the need for a consistent RESTful API became more and more important, and we simply could not wait for v2 (which hadn't and still hasn't, a release date) to drop.</p>
|
||||
<p>So <a href="https://github.com/NodeBB/NodeBB/pull/8708/commits/3dc9324ed60bc2e331b22dc70e0e253e0d3eaf62">two months ago</a>, I started pulling out this work to a separate branch based off of <code>master</code>, and set about to finishing the integration. I'm proud to say that the preliminary release of this API has been merged into core, and is available starting v1.15.0.</p>
|
||||
<p>Better yet, <a href="https://blog.nodebb.org/unveiling-of-the-read-api/">the docs have been much improved, and are now maintained similar to the Read API</a>, using the OpenAPI v3 format, and can be found here ?</p>
|
||||
<p><a href="//docs.nodebb.org/api/write">Write API Documentation</a></p>
|
||||
<h2 id="isntwriteapiamisnomer">Isn't "Write API" a misnomer?</h2>
|
||||
<p>Yes! In a sense, it is a transitionary title while the API evolves over time.</p>
|
||||
<ul>
|
||||
<li>The <strong>Read API</strong>, such as it is, contains a number of non-<code>GET</code> routes
|
||||
<ul>
|
||||
<li>These would mostly be upload-specific routes (avatars, cover photos, topic thumbnails, etc.)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>The <strong>Write API</strong>, on the other hand, <em>does</em> contain a couple of <code>GET</code> routes, and will be gaining more, over time.</li>
|
||||
</ul>
|
||||
<p>The nomenclature comes from its predecessor, <a href="//github.com/NodeBB/nodebb-plugin-write-api"><code>nodebb-plugin-write-api</code></a>. I wanted to keep the feature name similar so as to reduce confusion.</p>
|
||||
<p>Over time, we intend to introduce additional GET routes, especially for new feature construction (or rewrites of such). For example, the <em>topic thumbnail</em> functionality is currently undergoing a rewrite, and will using the Write API exclusively.</p>
|
||||
<p>All routes using the new Write API are visually separate from the Read API routes in that they all begin with <code>/api/v3</code> (e.g. <code>POST api/v3/topics</code> to create a new topic.)</p>
|
||||
<p>Eventually, the plan is to rename the <em>Write API</em> into the <em>REST API</em> (imaginative, I know.)</p>
|
||||
<h2 id="doesthismeanthedeathoftheapiprefix">Does this mean the death of the <code>/api</code> prefix?</h2>
|
||||
<p>Not at all! The existing API will continue to be maintained. <strong>We have no plans to deprecate this API</strong>. It will likely be renamed from <em>Read API</em> to <em>Page API</em> or similar. This is purely for aesthetics.</p>
|
||||
<p>Oftentimes, we would tell clients that any page you can browse to, you can see its underlying data by prepending <code>/api</code> onto that page's path. This remains true today, but we also want the reverse to be true. This means additional routes would be removed from the Read API and ported over to the Write API.</p>
|
||||
<p>NodeBB's existing frontend uses the Read API exclusively to render page templates, and to that end the Read API is functioning as intended. The Write API was originally intended to allow external services (and specific usage from within NodeBB) leaner access to forum resources.</p>
|
||||
<h2 id="whatswiththeapiv3prefix">What's with the <code>/api/v3</code> prefix?</h2>
|
||||
<p>My plan with the Write API was to iterate on version numbers whenever there were breaking changes. There was one major change, which brought the plugin's prefix to <code>api/v2</code>, and for this move, we will be bumping it <strong>one last time</strong> (more on why below) to <code>api/v3</code>. I chose to continue with <code>api/v3</code> as it would not conflict with users who wished to use the Write API plugin in parallel with a newer version of NodeBB.</p>
|
||||
<p>Keep in mind: Most users of the Write API should be able to migrate to <code>v3</code> with no significant issues.</p>
|
||||
<p><a href="https://github.com/NodeBB/NodeBB/pull/8708#issuecomment-705780711">The full list of breaking changes from <code>v2</code> to <code>v3</code> can be found here</a>.</p>
|
||||
<p>Eventually, I intend to introduce a versioning system for the API, <a href="https://stripe.com/blog/api-versioning">inspired by the Stripe API team</a>. They release new versions of their API prolifically, versioned by the date (e.g. <code>2017-05-24</code>), and requests sent in to the API with that specific version number will instruct Stripe to mutate the response to match what the response would have looked like under that version. It absolutely blows my mind how that worked, and my plan is to mimic something similar for successive releases of the API.</p>
|
||||
<h2 id="howdoesthisplaywithnodebbswebsocketimplementation">How does this play with NodeBB's websocket implementation?</h2>
|
||||
<p>The websockets/socket.io interface was never meant to be for external use. We rely on websockets for real-time events, such as new posts, notifications, online indicators, etc.</p>
|
||||
<p>I fully admit (on behalf of my peers Baris and Andrew) that we abused this system to also handle simpler requests for information, or to instruct the server to do things.</p>
|
||||
<p>These <em>call-and-response</em> type of requests were not what socket.io was designed for. That said, it handles these types of actions without breaking a sweat, but why reinvent the wheel when a battle-hardened version exists in the form of HTTP requests?</p>
|
||||
<p>Socket.io even falls back to using XHR when a websocket connection is not available, imagine that!</p>
|
||||
<p><strong>Let me be clear</strong>, we have no intention of dropping our use of socket.io. When we implement real-time events on websockets/socket.io, they work really really well, and it would be much easier to treat our use of websockets simply as a form of progressive enhancement, rather than an integral part of our software which won't work without it.</p>
|
||||
<p>There is one (rather significant) additional reason why I personally wanted to switch to using a REST API — many of our clients don't have the technical knowledge to interface via websockets. This was the original reason why the Write API was created, but if a client wanted functionality that wasn't provided by that API, they were essentially SOL because domain knowledge on how to connect to a websocket server was far and few between. HTTP is a no-brainer, and many <em>many</em> developers know how to handle and negotiate responses with external APIs.</p>
|
||||
<p>As an extension to that reason — if clients couldn't easily figure it out, <strong>neither could (or would) white-hat hackers</strong>. This meant that we had a blind spot in our websocket implementation, in that fewer eyes were looking at the code, and fewer people were trying to break in, simply due to the relative obscurity of websockets. While security by obscurity can be a part of your security plan, relying solely on security by obscurity is <strong>plain bad practice</strong>. The easier it is to interface with NodeBB, the easier it will be for clients to use, and for us to feel assured that it is implemented in a safe way.</p>
|
||||
<h2 id="theremaining10">The remaining 10%</h2>
|
||||
<p>Getting the rest of the call-and-response usages of websockets migrated to the Write API is the last 10%, and of course, the 10% is the hardest and dullest work. Once we're done this, we'll likely move forward with the name change from <strong>Write API</strong> to <strong>REST API</strong>.</p>
|
||||
<p>The average end-user of NodeBB shouldn't see a single bit of difference in their every day use. If nobody can see what I'm doing, I'd consider the migration a success ?</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-5"/>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<div class="d-flex flex-column flex-md-row gap-5 justify-content-between mb-5 flex-wrap">
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Get Started</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/product" class="link-secondary text-decoration-none">PRODUCT</a></li>
|
||||
<li><a href="/pricing" class="link-secondary text-decoration-none">PRICING</a></li>
|
||||
<li><a href="/services" class="link-secondary text-decoration-none">SERVICES</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Resources</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="https://community.nodebb.org/" class="link-secondary text-decoration-none" target="_blank">COMMUNITY</a></li>
|
||||
<li><a href="https://try.nodebb.org/" class="link-secondary text-decoration-none" target="_blank">DEMO SITE</a></li>
|
||||
<li><a href="https://community.nodebb.org/category/28/answers" class="link-secondary text-decoration-none" target="_blank">ANSWERS</a></li>
|
||||
<li><a href="https://docs.nodebb.org" class="link-secondary text-decoration-none" target="_blank">DOCUMENTATION</a></li>
|
||||
<li><a href="/bounty" class="link-secondary text-decoration-none">BUG BOUNTY</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Company</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/about" class="link-secondary text-decoration-none">ABOUT</a></li>
|
||||
<li><a href="/blog" class="link-secondary text-decoration-none">BLOG</a></li>
|
||||
<li><a href="/contact" class="link-secondary text-decoration-none">CONTACT</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<a href="https://manage.nodebb.org/register" class="btn btn-primary"><i class="fa-solid fa-rocket"></i> Start Free Trial</a>
|
||||
<div class="d-flex gap-3 mt-3 justify-content-between px-2">
|
||||
<a title="NodeBB Github Page" href="https://github.com/nodebb/nodebb" class="link-secondary text-decoration-none"><i class="fab fa-github"></i></a>
|
||||
<a title="NodeBB Twitter Page" href="https://twitter.com/nodebb" class="link-secondary text-decoration-none"><i class="fab fa-twitter"></i></a>
|
||||
<a title="NodeBB Mastodon Page" href="https://fosstodon.org/@nodebb" class="link-secondary text-decoration-none"><i class="fa-brands fa-mastodon"></i></a>
|
||||
<a title="NodeBB Facebook Page" href="https://www.facebook.com/NodeBB" class="link-secondary text-decoration-none"><i class="fab fa-facebook"></i></a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap justify-content-between gap-5 small">
|
||||
<span class="text-secondary text-nowrap">©2025 NodeBB, Inc. — Made in Canada with <i class="fa-solid fa-heart text-danger"></i>.</span>
|
||||
<div class="d-flex gap-3">
|
||||
<a href="/tos" class="link-secondary text-nowrap text-decoration-none">Terms of Service</a>
|
||||
<a href="/privacy" class="link-secondary text-nowrap text-decoration-none">Privacy Policy</a>
|
||||
<a href="/gdpr" class="link-secondary text-nowrap text-decoration-none">GDPR</a>
|
||||
<a href="/dmca" class="link-secondary text-nowrap text-decoration-none">DMCA</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap 5 JS Bundle (includes Popper) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
181
blog/the-faceless-master.html
Normal file
|
@ -0,0 +1,181 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>The Faceless Master - NodeBB - Modern Community Forum Software</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Blog of NodeBB Forum Software - The Modern Discussion Platform">
|
||||
<meta name="author" content="NodeBB Inc.">
|
||||
<meta name="keywords" content="nodebb, node.js, forum, discussion, community, software, hosting, blog">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/icons/32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/icons/16x16.png">
|
||||
<link rel="shortcut icon" href="/images/icons/favicon.ico">
|
||||
|
||||
<!-- Google Fonts: Inter & Poppins -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Poppins:wght@400;600&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<!-- our css-->
|
||||
<link href="/css/style.css" rel="stylesheet">
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-V0P62EB8Q6"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-V0P62EB8Q6');
|
||||
</script>
|
||||
</head>
|
||||
<body class="p-0 py-5 p-lg-5">
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar navbar-expand-lg bg-body fixed-top shadow-sm">
|
||||
<div class="container-lg">
|
||||
<a class="navbar-brand py-2" href="/">
|
||||
<img src="/images/brand/nodebb-logo.svg" style="height: 36px; width: auto;" alt="NodeBB Logo" />
|
||||
</a>
|
||||
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMenu" aria-controls="navbarMenu" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse justify-content-end" id="navbarMenu">
|
||||
<ul class="nav nav-underline gap-4 flex-column flex-lg-row align-items-end align-items-lg-center mt-4 mt-lg-0">
|
||||
<li class="nav-item">
|
||||
<a href="/" class="nav-link text-reset fw-semibold">HOME</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/product" class="nav-link text-reset fw-semibold">PRODUCT</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/pricing" class="nav-link text-reset fw-semibold">PRICING</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/services" class="nav-link text-reset fw-semibold">SERVICES</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" target="_blank" role="button" data-bs-toggle="dropdown" aria-expanded="false" class="nav-link dropdown-toggle text-reset fw-semibold">RESOURCES</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end border-0 shadow p-1">
|
||||
<li><a href="https://community.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Community</a></li>
|
||||
<li><a href="https://try.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Demo Site</a></li>
|
||||
<li><a href="https://community.nodebb.org/category/28/answers" class="dropdown-item rounded-1" target="_blank">Answers</a></li>
|
||||
<li><a href="https://docs.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Documentation</a></li>
|
||||
<li><a href="/bounty" class="dropdown-item rounded-1">Bug Bounty</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/contact" class="btn btn-warning rounded-pill">Contact Us</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="https://manage.nodebb.org/register" class="btn btn-primary"><i class="fa-solid fa-rocket"></i> Start Free Trial</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="container-lg mt-5">
|
||||
<!-- Home -->
|
||||
<div id="home-tab-pane">
|
||||
<div class="row pt-2 pt-lg-5">
|
||||
<div class="col-12 d-flex flex-column gap-4">
|
||||
<h1 class="display-1 fw-bold fs-1">
|
||||
The Faceless Master
|
||||
</h1>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- blog post -->
|
||||
<div class="py-5">
|
||||
<div class="d-flex gap-2 align-items-center mb-4">
|
||||
<a href="https://community.nodebb.org/user/signal_rorak"><img class="rounded-circle" width="32" src="https://community.nodebb.org/assets/uploads/profile/uid-2/2-profileavatar-1738544541106.jpeg"></a> <a href="https://community.nodebb.org/user/signal_rorak" class="fw-semibold">Nick Weiner</a> <span class="text-secondary">7/27/2020, 1:08:30 PM</span>
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="kg-card-markdown">
|
||||
|
||||
<p>This past month, my family asked me to design a website to showcase my late grandfather’s paintings. Grandpa passed away in ’93, the same year that I was born. We visited him when I was a baby and he held me. There’s a picture of it somewhere… but I never had the chance to really know him. When I was first asked to put his website together, I felt compelled to work on it primarily because I had the most technical experience of anyone in my family in web design. But not long after beginning the project, unexpectedly, it became a personally emotional journey. While working on the site, I enjoyed looking at Grandpa’s art and choosing each spot for his paintings on the site’s gallery. He was quite the painter in his time. Once featured in TIME Magazine in 1950, Grandpa won several prestigious artistic awards, with pieces showcased in New York, Pittsburgh, Chicago and Washington D.C. As I designed his site, I found the time I had with each of his paintings to be a visually moving experience. When viewing his artwork, I could look at it and exist in a moment just as I did laying in his lap 27 years ago as a baby, not really knowing him, but seeing him, if only for a brief moment.</p>
|
||||
|
||||
<p>Often times when people create forums, we begin with a basic technical reason why. Forums offer places for our users to hangout, discuss the platform, etc. ‘The users are the center of everything we do’. At times, though, the forums we create become quite a bit more than we initially anticipated. When users are viewed as people, people become friends, and in that space, a community develops and thrives.</p>
|
||||
|
||||
<p>My name is Nick Weiner and I grew up using forums such as Neopets, Gaia Online, and various phpBB-based communities. Like many of you, forums were my connection to many friends I would never have the opportunity to meet in person. Speaking at a conference in 2008, former Gaia Online CEO, Craig Sherman, said “Anonymous forums allow an extra level of getting to separate yourself from your core who you are and allow a little less consequences, and therefore, you can connect with like-minded souls in a way that’s harder to in the real world”. Sherman explained it as a similar experience to meeting someone on an airplane, opening up with them about everything in your life, and doing so because you knew then that you would likely never see them again (SGS,2008). In that core separation when our identity is separated from real consequences there are new opportunities to act on all sorts of impulses. Anonymity gives every person who uses forums some power over their identity.</p>
|
||||
|
||||
|
||||
<p>Despite never knowing my grandpa, he left behind 63 paintings, each of which conveyed a full range of emotions, passion, understanding, thoughts, and feelings. I found him as a faceless master who chose to share his identity in a profoundly beautiful way. Through his artwork, I was able to deeply connect with him 27 years after he passed away. I believe that, despite the way we have previously thought about our forums, it is time that we take a moment to reflect on what each person who uses our forums is trying to convey while using our sites as a master of their own identities. John F. Kennedy once said “So, let us not be blind to our differences -- but let us also direct attention to our common interests. Our most basic common link is that we all inhabit this small planet. We all breathe the same air. We all cherish our children's future. And we are all mortal”. It changes everything knowing that because of our anonymity, we can actively celebrate identity and understand the behaviors of those within our communities. Forums allow us to be artists and masters of our own identities. Knowing that, no matter where we are presently, each of us now have the opportunity to create stories that will reveal to our grandchildren what we were all about.</p>
|
||||
|
||||
<p>
|
||||
References:
|
||||
<a href="http://www.abeweinerpaintings.com">Abe Weiner Paintings</a>
|
||||
<a href="https://www.youtube.com/watch?v=NHoK19DRnT4">SGS2008: Casual MMOs and Immersive Worlds</a>
|
||||
Cover Photo by <a href="https://unsplash.com/photos/WukitUSJRgY">Warren Wong on Unsplash</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-5"/>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<div class="d-flex flex-column flex-md-row gap-5 justify-content-between mb-5 flex-wrap">
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Get Started</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/product" class="link-secondary text-decoration-none">PRODUCT</a></li>
|
||||
<li><a href="/pricing" class="link-secondary text-decoration-none">PRICING</a></li>
|
||||
<li><a href="/services" class="link-secondary text-decoration-none">SERVICES</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Resources</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="https://community.nodebb.org/" class="link-secondary text-decoration-none" target="_blank">COMMUNITY</a></li>
|
||||
<li><a href="https://try.nodebb.org/" class="link-secondary text-decoration-none" target="_blank">DEMO SITE</a></li>
|
||||
<li><a href="https://community.nodebb.org/category/28/answers" class="link-secondary text-decoration-none" target="_blank">ANSWERS</a></li>
|
||||
<li><a href="https://docs.nodebb.org" class="link-secondary text-decoration-none" target="_blank">DOCUMENTATION</a></li>
|
||||
<li><a href="/bounty" class="link-secondary text-decoration-none">BUG BOUNTY</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Company</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/about" class="link-secondary text-decoration-none">ABOUT</a></li>
|
||||
<li><a href="/blog" class="link-secondary text-decoration-none">BLOG</a></li>
|
||||
<li><a href="/contact" class="link-secondary text-decoration-none">CONTACT</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<a href="https://manage.nodebb.org/register" class="btn btn-primary"><i class="fa-solid fa-rocket"></i> Start Free Trial</a>
|
||||
<div class="d-flex gap-3 mt-3 justify-content-between px-2">
|
||||
<a title="NodeBB Github Page" href="https://github.com/nodebb/nodebb" class="link-secondary text-decoration-none"><i class="fab fa-github"></i></a>
|
||||
<a title="NodeBB Twitter Page" href="https://twitter.com/nodebb" class="link-secondary text-decoration-none"><i class="fab fa-twitter"></i></a>
|
||||
<a title="NodeBB Mastodon Page" href="https://fosstodon.org/@nodebb" class="link-secondary text-decoration-none"><i class="fa-brands fa-mastodon"></i></a>
|
||||
<a title="NodeBB Facebook Page" href="https://www.facebook.com/NodeBB" class="link-secondary text-decoration-none"><i class="fab fa-facebook"></i></a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap justify-content-between gap-5 small">
|
||||
<span class="text-secondary text-nowrap">©2025 NodeBB, Inc. — Made in Canada with <i class="fa-solid fa-heart text-danger"></i>.</span>
|
||||
<div class="d-flex gap-3">
|
||||
<a href="/tos" class="link-secondary text-nowrap text-decoration-none">Terms of Service</a>
|
||||
<a href="/privacy" class="link-secondary text-nowrap text-decoration-none">Privacy Policy</a>
|
||||
<a href="/gdpr" class="link-secondary text-nowrap text-decoration-none">GDPR</a>
|
||||
<a href="/dmca" class="link-secondary text-nowrap text-decoration-none">DMCA</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap 5 JS Bundle (includes Popper) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
242
blog/unveiling-of-the-read-api.html
Normal file
|
@ -0,0 +1,242 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Unveiling of the Read API - NodeBB - Modern Community Forum Software</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Blog of NodeBB Forum Software - The Modern Discussion Platform">
|
||||
<meta name="author" content="NodeBB Inc.">
|
||||
<meta name="keywords" content="nodebb, node.js, forum, discussion, community, software, hosting, blog">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/icons/32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/icons/16x16.png">
|
||||
<link rel="shortcut icon" href="/images/icons/favicon.ico">
|
||||
|
||||
<!-- Google Fonts: Inter & Poppins -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Poppins:wght@400;600&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<!-- our css-->
|
||||
<link href="/css/style.css" rel="stylesheet">
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-V0P62EB8Q6"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-V0P62EB8Q6');
|
||||
</script>
|
||||
</head>
|
||||
<body class="p-0 py-5 p-lg-5">
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar navbar-expand-lg bg-body fixed-top shadow-sm">
|
||||
<div class="container-lg">
|
||||
<a class="navbar-brand py-2" href="/">
|
||||
<img src="/images/brand/nodebb-logo.svg" style="height: 36px; width: auto;" alt="NodeBB Logo" />
|
||||
</a>
|
||||
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMenu" aria-controls="navbarMenu" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse justify-content-end" id="navbarMenu">
|
||||
<ul class="nav nav-underline gap-4 flex-column flex-lg-row align-items-end align-items-lg-center mt-4 mt-lg-0">
|
||||
<li class="nav-item">
|
||||
<a href="/" class="nav-link text-reset fw-semibold">HOME</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/product" class="nav-link text-reset fw-semibold">PRODUCT</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/pricing" class="nav-link text-reset fw-semibold">PRICING</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/services" class="nav-link text-reset fw-semibold">SERVICES</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" target="_blank" role="button" data-bs-toggle="dropdown" aria-expanded="false" class="nav-link dropdown-toggle text-reset fw-semibold">RESOURCES</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end border-0 shadow p-1">
|
||||
<li><a href="https://community.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Community</a></li>
|
||||
<li><a href="https://try.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Demo Site</a></li>
|
||||
<li><a href="https://community.nodebb.org/category/28/answers" class="dropdown-item rounded-1" target="_blank">Answers</a></li>
|
||||
<li><a href="https://docs.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Documentation</a></li>
|
||||
<li><a href="/bounty" class="dropdown-item rounded-1">Bug Bounty</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/contact" class="btn btn-warning rounded-pill">Contact Us</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="https://manage.nodebb.org/register" class="btn btn-primary"><i class="fa-solid fa-rocket"></i> Start Free Trial</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="container-lg mt-5">
|
||||
<!-- Home -->
|
||||
<div id="home-tab-pane">
|
||||
<div class="row pt-2 pt-lg-5">
|
||||
<div class="col-12 d-flex flex-column gap-4">
|
||||
<h1 class="display-1 fw-bold fs-1">
|
||||
Unveiling of the Read API
|
||||
</h1>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- blog post -->
|
||||
<div class="py-5">
|
||||
<div class="d-flex gap-2 align-items-center mb-4">
|
||||
<a href="https://community.nodebb.org/user/julian"><img class="rounded-circle" width="32" src="https://community.nodebb.org/assets/uploads/profile/uid-2/2-profileavatar-1738544541106.jpeg"></a> <a href="https://community.nodebb.org/user/julian" class="fw-semibold">Julian Lam</a> <span class="text-secondary">6/1/2020, 12:32:09 PM</span>
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="kg-card-markdown">
|
||||
|
||||
Developer empowerment has always been at the core of NodeBB:
|
||||
<ul>
|
||||
<li>Plugins receive first-class treatment, in that there are plenty of hooks enabling plugins to interact with nearly all facets of NodeBB</li>
|
||||
<li>Themes play a central role in NodeBB, allowing designers to fully realise their vision without being hamstrung by unnecessary limitations.</li>
|
||||
<li>Our open source community is thriving due in no small part to our community forum of developers helping each other</li>
|
||||
</ul>
|
||||
However, one consistent pain point has been our API, and its rather opaque discovery model.
|
||||
|
||||
More often than I care to admit, we'll receive a query: "Where are your Read API docs?", to which our response is a conciliatory "prepend <code>/api</code> to any route in order to see the underlying JSON". After all, we use the same APIs to render every single page in NodeBB, and if it's good enough for us, then it ought to be good enough for you!
|
||||
|
||||
However, this sort of solution totally sucks (pardon my French) if you don't have experience working <strong>with</strong> NodeBB. The core developers all have the full context behind most parts of the code, so we know the full capabilities of the backend, and where to look when we want to edit something. On the other hand, developers new to NodeBB don't have this benefit, and given that we're not a small project, it can definitely feel overwhelming to try to find out how everything works, or where specific logic lives in NodeBB.
|
||||
|
||||
The lack of consistent API documentation also implicitly enforces a "develop-first" approach, where you simply get started on a feature by diving into the code and figure out the plan and design later. While this approach works really well for <em>hackers</em><sup>1</sup>, it lets down the <em>planners</em>, and we want to take steps to ease development for both types of developers.
|
||||
|
||||
Another downside of our pre-existing API is that it is not entirely RESTful. The API has been <code>GET</code>-exclusive for the most part. Whatever few places we don't do <code>GET</code>s are for uploads and non-JSON related exports. Almost every action to write to NodeBB is done via websockets, which we are intending to partially deprecate<sup>2</sup>.
|
||||
|
||||
Lastly, our API is mostly displaying what we need on the page (to render properly client-side). In many cases, that dovetails nicely with developer expectations, but the nomenclature and structure need revising in order to follow API best-practices. A quick example would be, <code>/users</code> shows the user list, but individual users are under the <code>/user</code> prefix (no plural).
|
||||
<h2 id="whatdo">What do?</h2>
|
||||
The first order of business is to shine a light on the black box, our opaque Read API.
|
||||
|
||||
The more documentation we have, the easier it will be to spot opportunities for improvement. More importantly, better documentation empowers developers with better context for their planning.
|
||||
|
||||
We settled on the OpenAPI 3 specification (formerly known as Swagger), for a couple of reasons:
|
||||
<ul>
|
||||
<li>A standardised API schema allows for consistent consumption via automated tooling</li>
|
||||
<li>We can achieve some spectacular out-of-the-box styling via <a href="https://github.com/Redocly/redoc">ReDoc</a>.</li>
|
||||
</ul>
|
||||
A huge thanks to <a href="https://github.com/akhoury">Aziz Khoury </a> for getting us started on this. He singlehandedly implemented a script that allowed for automatic generation of OpenAPI docs (including request and response schemas) by examining the content that went in and out of our <a href="//community.nodebb.org">community forum</a>. Right off the bat, we had nearly complete, workable documentation! ??
|
||||
|
||||
The rest of it<sup>3</sup> was done by hand by the core developers, and <a href="https://github.com/NodeBB/NodeBB/blob/master/public/openapi/read.yaml">the document itself</a> will be a living document, that grows with the project.
|
||||
|
||||
So without further ado, introducing the <a href="https://docs.nodebb.org/api/">NodeBB Read API Documentation</a>! ??
|
||||
<h2 id="sowhat">So what?</h2>
|
||||
If you <a href="https://github.com/NodeBB/NodeBB/blob/master/public/openapi/read.yaml">look at the actual spec</a>, it's not really all that exciting. It's a bunch of YAML that looks nigh-impenetrable to the laymen<sup>4</sup>.
|
||||
|
||||
The real cool advantages come in two stages:
|
||||
<h3 id="1redoc">1. ReDoc</h3>
|
||||
We integrated ReDoc to style our documentation. We loved the design and found that it gave our API some much needed polish. <a href="https://docs.nodebb.org/api/">If you go to our Read API documentation now</a>, you'll see ReDoc as the front-end serving the behind-the-scenes YAML file.
|
||||
<h3 id="2testingsuiteintegration">2. Testing Suite Integration</h3>
|
||||
The OpenAPI document itself is tied in to our testing suite, and so every time a change is made to the public API, a corresponding change <strong>must</strong> be made to the OpenAPI document as well, otherwise the tests will fail.
|
||||
|
||||
This step is important because it keeps us accountable and ensures that the documentation itself is up-to-date with the latest code. Documentation is often one of the hardest things to keep up-to-date, second only to <strong>writing</strong> the actual documentation, so you can imagine why a tool is needed to gently (or in some cases, no-so-gently) remind developers to keep the API documentation up to date!
|
||||
<h3 id="3betterchangetracking">3. Better change tracking</h3>
|
||||
We will now also be able to see how the API changes between version releases, which begets the question of whether this aids us in our steps towards adopting <a href="https://semver.org/">semver</a>.
|
||||
<h2 id="letstakeastepback">Let's take a step back...</h2>
|
||||
In March, <a href="https://blog.nodebb.org/slice-and-dice-ndash-bringing-nodebb-back-to-basics/">I talked about taking a step back to view a project under a more objective lens</a>. Oftentimes developers (myself included!) will dive so deeply into a feature or project, and become so invested in its success, that we don't often consider whether it (or its implementation) is a good idea in the first place.
|
||||
|
||||
So let's take the time now to take a step back and view the OpenAPI spec under an objective lens:
|
||||
|
||||
I will admit that initial adoption is painful. The integration into our testing suite will introduce a failure point during our development workflow).
|
||||
|
||||
However, the benefits potentially outnumber the downsides:
|
||||
<ul>
|
||||
<li>We should now be able to catch bugs where output is changed by accident, or if property types have changed.</li>
|
||||
<li>We gain savings in developer frustration, as new developers try to integrate with NodeBB for the first time.</li>
|
||||
</ul>
|
||||
Time will tell as to whether we will discover more benefits, or run into additional downsides. We are hoping that the Read API documentation is a step in the right direction for all parties involved.
|
||||
<h2 id="whatsnext">What's next?</h2>
|
||||
Now that we have a published OpenAPI spec for our Read API, the obvious next step would be to adopt this for the Write API as well.
|
||||
|
||||
Our Write API capabilities are currently served by our <a href="https://github.com/NodeBB/nodebb-plugin-write-api">Write API plugin</a>. Currently, we're in the planning and initial development stages of merging the Write API into core. This is <a href="https://blog.nodebb.org/looking-ahead-to-nodebb-v2-x/">scheduled to land in NodeBB v2</a>.
|
||||
|
||||
The <a href="https://github.com/NodeBB/nodebb-plugin-write-api/blob/master/routes/v2/readme.md">current documentation is maintained by hand</a>, and does not offer any of the benefits of OpenAPI.
|
||||
|
||||
Additionally, while the majority of the "write-type" calls in NodeBB are handled via socket.io, we intend to replace these calls with calls to the Write API.
|
||||
|
||||
A potential stretch goal here would be strict versioning of the API with built-in backwards compatibility. We want to model this after Stripe's API, which allows you to pass in outdated payloads under a version header, and receive the response as though you were still calling that outdated API.
|
||||
|
||||
====
|
||||
|
||||
<sup>1</sup> "hacker" in this context, refers to the mindset of figuring out how something works in order to extend or modify it. In modern times, the term has evolved to mean someone gaining unauthorized access to a resource, usually with malicious intent, but I am using it in the strictly exploratory meaning.
|
||||
<sup>2</sup> <strong><em>WHAT?!</em></strong>, you say? All will be explained, in a future blog post. Don't worry, socket.io won't go away, we're just reinforcing our commitment to the Write API.
|
||||
<sup>3</sup> Pun not intended.
|
||||
<sup>4</sup> For what it's worth, it's pretty impenetrable by developers too.
|
||||
<sup>Cover Photo</sup> <a href="https://unsplash.com/photos/_-hjiem5TqI">Sincerely Media on Unsplash</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-5"/>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<div class="d-flex flex-column flex-md-row gap-5 justify-content-between mb-5 flex-wrap">
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Get Started</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/product" class="link-secondary text-decoration-none">PRODUCT</a></li>
|
||||
<li><a href="/pricing" class="link-secondary text-decoration-none">PRICING</a></li>
|
||||
<li><a href="/services" class="link-secondary text-decoration-none">SERVICES</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Resources</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="https://community.nodebb.org/" class="link-secondary text-decoration-none" target="_blank">COMMUNITY</a></li>
|
||||
<li><a href="https://try.nodebb.org/" class="link-secondary text-decoration-none" target="_blank">DEMO SITE</a></li>
|
||||
<li><a href="https://community.nodebb.org/category/28/answers" class="link-secondary text-decoration-none" target="_blank">ANSWERS</a></li>
|
||||
<li><a href="https://docs.nodebb.org" class="link-secondary text-decoration-none" target="_blank">DOCUMENTATION</a></li>
|
||||
<li><a href="/bounty" class="link-secondary text-decoration-none">BUG BOUNTY</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Company</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/about" class="link-secondary text-decoration-none">ABOUT</a></li>
|
||||
<li><a href="/blog" class="link-secondary text-decoration-none">BLOG</a></li>
|
||||
<li><a href="/contact" class="link-secondary text-decoration-none">CONTACT</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<a href="https://manage.nodebb.org/register" class="btn btn-primary"><i class="fa-solid fa-rocket"></i> Start Free Trial</a>
|
||||
<div class="d-flex gap-3 mt-3 justify-content-between px-2">
|
||||
<a title="NodeBB Github Page" href="https://github.com/nodebb/nodebb" class="link-secondary text-decoration-none"><i class="fab fa-github"></i></a>
|
||||
<a title="NodeBB Twitter Page" href="https://twitter.com/nodebb" class="link-secondary text-decoration-none"><i class="fab fa-twitter"></i></a>
|
||||
<a title="NodeBB Mastodon Page" href="https://fosstodon.org/@nodebb" class="link-secondary text-decoration-none"><i class="fa-brands fa-mastodon"></i></a>
|
||||
<a title="NodeBB Facebook Page" href="https://www.facebook.com/NodeBB" class="link-secondary text-decoration-none"><i class="fab fa-facebook"></i></a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap justify-content-between gap-5 small">
|
||||
<span class="text-secondary text-nowrap">©2025 NodeBB, Inc. — Made in Canada with <i class="fa-solid fa-heart text-danger"></i>.</span>
|
||||
<div class="d-flex gap-3">
|
||||
<a href="/tos" class="link-secondary text-nowrap text-decoration-none">Terms of Service</a>
|
||||
<a href="/privacy" class="link-secondary text-nowrap text-decoration-none">Privacy Policy</a>
|
||||
<a href="/gdpr" class="link-secondary text-nowrap text-decoration-none">GDPR</a>
|
||||
<a href="/dmca" class="link-secondary text-nowrap text-decoration-none">DMCA</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap 5 JS Bundle (includes Popper) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
181
blog/what-does-a-forum-migration-look-like-just-ask-moz.html
Normal file
|
@ -0,0 +1,181 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>What does a forum migration look like? Just ask Moz... - NodeBB - Modern Community Forum Software</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Blog of NodeBB Forum Software - The Modern Discussion Platform">
|
||||
<meta name="author" content="NodeBB Inc.">
|
||||
<meta name="keywords" content="nodebb, node.js, forum, discussion, community, software, hosting, blog">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/icons/32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/icons/16x16.png">
|
||||
<link rel="shortcut icon" href="/images/icons/favicon.ico">
|
||||
|
||||
<!-- Google Fonts: Inter & Poppins -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Poppins:wght@400;600&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<!-- our css-->
|
||||
<link href="/css/style.css" rel="stylesheet">
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-V0P62EB8Q6"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-V0P62EB8Q6');
|
||||
</script>
|
||||
</head>
|
||||
<body class="p-0 py-5 p-lg-5">
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar navbar-expand-lg bg-body fixed-top shadow-sm">
|
||||
<div class="container-lg">
|
||||
<a class="navbar-brand py-2" href="/">
|
||||
<img src="/images/brand/nodebb-logo.svg" style="height: 36px; width: auto;" alt="NodeBB Logo" />
|
||||
</a>
|
||||
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMenu" aria-controls="navbarMenu" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse justify-content-end" id="navbarMenu">
|
||||
<ul class="nav nav-underline gap-4 flex-column flex-lg-row align-items-end align-items-lg-center mt-4 mt-lg-0">
|
||||
<li class="nav-item">
|
||||
<a href="/" class="nav-link text-reset fw-semibold">HOME</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/product" class="nav-link text-reset fw-semibold">PRODUCT</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/pricing" class="nav-link text-reset fw-semibold">PRICING</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/services" class="nav-link text-reset fw-semibold">SERVICES</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" target="_blank" role="button" data-bs-toggle="dropdown" aria-expanded="false" class="nav-link dropdown-toggle text-reset fw-semibold">RESOURCES</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end border-0 shadow p-1">
|
||||
<li><a href="https://community.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Community</a></li>
|
||||
<li><a href="https://try.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Demo Site</a></li>
|
||||
<li><a href="https://community.nodebb.org/category/28/answers" class="dropdown-item rounded-1" target="_blank">Answers</a></li>
|
||||
<li><a href="https://docs.nodebb.org/" class="dropdown-item rounded-1" target="_blank">Documentation</a></li>
|
||||
<li><a href="/bounty" class="dropdown-item rounded-1">Bug Bounty</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/contact" class="btn btn-warning rounded-pill">Contact Us</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="https://manage.nodebb.org/register" class="btn btn-primary"><i class="fa-solid fa-rocket"></i> Start Free Trial</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="container-lg mt-5">
|
||||
<!-- Home -->
|
||||
<div id="home-tab-pane">
|
||||
<div class="row pt-2 pt-lg-5">
|
||||
<div class="col-12 d-flex flex-column gap-4">
|
||||
<h1 class="display-1 fw-bold fs-1">
|
||||
What does a forum migration look like? Just ask Moz...
|
||||
</h1>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- blog post -->
|
||||
<div class="py-5">
|
||||
<div class="d-flex gap-2 align-items-center mb-4">
|
||||
<a href="https://community.nodebb.org/user/julian"><img class="rounded-circle" width="32" src="https://community.nodebb.org/assets/uploads/profile/uid-2/2-profileavatar-1738544541106.jpeg"></a> <a href="https://community.nodebb.org/user/julian" class="fw-semibold">Julian Lam</a> <span class="text-secondary">8/20/2021, 11:20:13 AM</span>
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="kg-card-markdown">
|
||||
<p>We were recently engaged by the SEO services company <a href="//moz.com">Moz</a> to re-vamp their Q&A forum. As is typical of many of our client engagements, not only do we do our best to migrate an existing forum to NodeBB, but we try to take any lessons we learn from the project, and apply it to all NodeBBs, going forward. In this case, Moz advised us on some new best-practices for migration work, and did an informal audit on our existing SEO implementations.</p>
|
||||
|
||||
<p>As always, the best way to build a presence and rank higher on search engines is to <strong>build quality content</strong>. Short of curated, handwritten content via a blog, the next best thing is to capture the buzz surrounding your product or organization, via a publicly indexable forum such as NodeBB. Search engines take many signals into account, beyond simple "linkbacks" and keywords – not only do we now need to ensure we publish top-quality content, we need to ensure it is both fresh and relevant as well.</p>
|
||||
|
||||
<p>Moz is by far the top reference when it comes to SEO research. In an ever-changing field, Moz manages to stay on top and deliver actionable content to webmasters worldwide. We are super excited to be hosting their Q&A forum, and wish the Moz team all the best.</p>
|
||||
|
||||
<p>Last week (on my birthday, no less ?!) they published a write-up on their migration to NodeBB, including some challenges and pitfalls. More importantly, they shared some metrics from before and after the migration, showing conclusive proof that it is possible to have your cake (a new community forum software) and eat it too (not lose traffic/market share)!</p>
|
||||
|
||||
<h6 id="checkouttheirwriteuphere"><a href="https://moz.com/blog/moz-qa-migration-case-study?utm_source=nodebb&utm_medium=blog">Check out their write-up here</a></h6>
|
||||
|
||||
<p>Thanks Moz!</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<sup>Cover Photo</sup> <a href="https://unsplash.com/@chiklad?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Ochir-Erdene Oyunmedeg</a> on <a href="https://unsplash.com/s/photos/green-grass?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-5"/>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<div class="d-flex flex-column flex-md-row gap-5 justify-content-between mb-5 flex-wrap">
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Get Started</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/product" class="link-secondary text-decoration-none">PRODUCT</a></li>
|
||||
<li><a href="/pricing" class="link-secondary text-decoration-none">PRICING</a></li>
|
||||
<li><a href="/services" class="link-secondary text-decoration-none">SERVICES</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Resources</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="https://community.nodebb.org/" class="link-secondary text-decoration-none" target="_blank">COMMUNITY</a></li>
|
||||
<li><a href="https://try.nodebb.org/" class="link-secondary text-decoration-none" target="_blank">DEMO SITE</a></li>
|
||||
<li><a href="https://community.nodebb.org/category/28/answers" class="link-secondary text-decoration-none" target="_blank">ANSWERS</a></li>
|
||||
<li><a href="https://docs.nodebb.org" class="link-secondary text-decoration-none" target="_blank">DOCUMENTATION</a></li>
|
||||
<li><a href="/bounty" class="link-secondary text-decoration-none">BUG BOUNTY</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="fw-bold">Company</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/about" class="link-secondary text-decoration-none">ABOUT</a></li>
|
||||
<li><a href="/blog" class="link-secondary text-decoration-none">BLOG</a></li>
|
||||
<li><a href="/contact" class="link-secondary text-decoration-none">CONTACT</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<a href="https://manage.nodebb.org/register" class="btn btn-primary"><i class="fa-solid fa-rocket"></i> Start Free Trial</a>
|
||||
<div class="d-flex gap-3 mt-3 justify-content-between px-2">
|
||||
<a title="NodeBB Github Page" href="https://github.com/nodebb/nodebb" class="link-secondary text-decoration-none"><i class="fab fa-github"></i></a>
|
||||
<a title="NodeBB Twitter Page" href="https://twitter.com/nodebb" class="link-secondary text-decoration-none"><i class="fab fa-twitter"></i></a>
|
||||
<a title="NodeBB Mastodon Page" href="https://fosstodon.org/@nodebb" class="link-secondary text-decoration-none"><i class="fa-brands fa-mastodon"></i></a>
|
||||
<a title="NodeBB Facebook Page" href="https://www.facebook.com/NodeBB" class="link-secondary text-decoration-none"><i class="fab fa-facebook"></i></a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap justify-content-between gap-5 small">
|
||||
<span class="text-secondary text-nowrap">©2025 NodeBB, Inc. — Made in Canada with <i class="fa-solid fa-heart text-danger"></i>.</span>
|
||||
<div class="d-flex gap-3">
|
||||
<a href="/tos" class="link-secondary text-nowrap text-decoration-none">Terms of Service</a>
|
||||
<a href="/privacy" class="link-secondary text-nowrap text-decoration-none">Privacy Policy</a>
|
||||
<a href="/gdpr" class="link-secondary text-nowrap text-decoration-none">GDPR</a>
|
||||
<a href="/dmca" class="link-secondary text-nowrap text-decoration-none">DMCA</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap 5 JS Bundle (includes Popper) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -280,6 +280,170 @@
|
|||
"image": "https://community.nodebb.org/assets/uploads/profile/uid-7950/7950-profileavatar-1701887954691.jpeg"
|
||||
},
|
||||
"content": "nodebb-v1-18-0-hanging-tough-with-a-new-release.html"
|
||||
},
|
||||
{
|
||||
"url": "https://nodebb.org/blog/what-does-a-forum-migration-look-like-just-ask-moz",
|
||||
"title": "What does a forum migration look like? Just ask Moz...",
|
||||
"excerpt": "We were recently engaged by the SEO services company Moz to re-vamp their Q&A forum. As is typical of many of our client engagements, not only do we do our...",
|
||||
"pubDate": "Fri, 20 Aug 2021 15:20:13 +0000",
|
||||
"cover": "https://images.unsplash.com/photo-1533460004989-cef01064af7e?q=80&w=1740&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
"author": {
|
||||
"name": "Julian Lam",
|
||||
"url": "https://community.nodebb.org/user/julian",
|
||||
"image": "https://community.nodebb.org/assets/uploads/profile/uid-2/2-profileavatar-1738544541106.jpeg"
|
||||
},
|
||||
"content": "what-does-a-forum-migration-look-like-just-ask-moz.html"
|
||||
},
|
||||
{
|
||||
"url": "https://nodebb.org/blog/nodebb-v1-17-0-scheduled-topics-new-moderation-features-and-more",
|
||||
"title": "NodeBB v1.17.0 – Scheduled Topics, New Moderation Features and More",
|
||||
"excerpt": "Spring has sprung in Toronto, so we’re taking advantage by getting some extremely low budget advertising. Hey, doesn’t everyone make their software purchasing decisions based on what they read on...",
|
||||
"pubDate": "Mon, 26 Apr 2021 20:56:03 +0000",
|
||||
"cover": "https://nodebb.org/blog/images/nodebb-v1-17-0-scheduled-topics-new-moderation-features-and-more.jpg",
|
||||
"author": {
|
||||
"name": "Jay Moonah",
|
||||
"url": "https://community.nodebb.org/user/jay-moonah",
|
||||
"image": "https://community.nodebb.org/assets/uploads/profile/uid-7950/7950-profileavatar-1701887954691.jpeg"
|
||||
},
|
||||
"content": "nodebb-v1-17-0-scheduled-topics-new-moderation-features-and-more.html"
|
||||
},
|
||||
{
|
||||
"url": "https://nodebb.org/blog/nodebb-v1-16-0-one-last-release-for-a-weird-year",
|
||||
"title": "NodeBB v1.16.0 – One Last Release For A Weird Year",
|
||||
"excerpt": "It’s not news to say 2020 has been… challenging. In Toronto, the home of NodeBB HQ, we’ve gone from a spring lockdown to a cautious summer reopening, to lockdown again...",
|
||||
"pubDate": "Thu, 24 Dec 2020 17:29:11 +0000",
|
||||
"cover": "https://nodebb.org/blog/images/nodebb-v1-16-0-one-last-release-for-a-weird-year-2048x1536.jpg",
|
||||
"author": {
|
||||
"name": "Jay Moonah",
|
||||
"url": "https://community.nodebb.org/user/jay-moonah",
|
||||
"image": "https://community.nodebb.org/assets/uploads/profile/uid-7950/7950-profileavatar-1701887954691.jpeg"
|
||||
},
|
||||
"content": "nodebb-v1-16-0-one-last-release-for-a-weird-year.html"
|
||||
},
|
||||
{
|
||||
"url": "https://nodebb.org/blog/the-api-continues-to-evolve",
|
||||
"title": "The API continues to evolve...",
|
||||
"excerpt": "A couple months back as part of our Roadmap to v2, I made the claim that one of the large features in that release would be the merging of the...",
|
||||
"pubDate": "Mon, 07 Dec 2020 17:36:00 +0000",
|
||||
"author": {
|
||||
"name": "Julian Lam",
|
||||
"url": "https://community.nodebb.org/user/julian",
|
||||
"image": "https://community.nodebb.org/assets/uploads/profile/uid-2/2-profileavatar-1738544541106.jpeg"
|
||||
},
|
||||
"content": "the-api-continues-to-evolve.html"
|
||||
},
|
||||
{
|
||||
"url": "https://nodebb.org/blog/optimizing-benchpress",
|
||||
"title": "Optimizing Benchpress",
|
||||
"excerpt": "Optimizing Benchpress Recently, I saw the release of nom v6 and decided I wanted to try it out, and see if I could speed up my hobby JS template compiler,...",
|
||||
"pubDate": "Mon, 23 Nov 2020 23:43:38 +0000",
|
||||
"author": {
|
||||
"name": "Peter Jaszkowiak",
|
||||
"url": "https://community.nodebb.org/user/pitaj",
|
||||
"image": "https://community.nodebb.org/assets/uploads/profile/uid-3076/3076-profileavatar-1639631260637.jpeg"
|
||||
},
|
||||
"content": "optimizing-benchpress.html"
|
||||
},
|
||||
{
|
||||
"url": "https://nodebb.org/blog/nodebb-1-15-0-home-again-but-still-hard-at-work",
|
||||
"title": "NodeBB 1.15.0 - Home Again But Still Hard At Work",
|
||||
"excerpt": "Unfortunately, after only returning a couple of times to our Toronto office, the city started to see a new spike in COVID-19 cases, so we’re back to working remotely. But...",
|
||||
"pubDate": "Sun, 08 Nov 2020 22:07:06 +0000",
|
||||
"cover": "nodebb-1-15-0-home-again-but-still-hard-at-work.jpg",
|
||||
"author": {
|
||||
"name": "Jay Moonah",
|
||||
"url": "https://community.nodebb.org/user/jay-moonah",
|
||||
"image": "https://community.nodebb.org/assets/uploads/profile/uid-7950/7950-profileavatar-1701887954691.jpeg"
|
||||
},
|
||||
"content": "nodebb-1-15-0-home-again-but-still-hard-at-work.html"
|
||||
},
|
||||
{
|
||||
"url": "https://nodebb.org/blog/forums-and-the-new-era-of-elearning",
|
||||
"title": "Forums and the new era of eLearning",
|
||||
"excerpt": "The ongoing coronavirus pandemic has changed the way we view eLearning. With students preparing to go back to school, parents, teachers, and students alike are wondering how this year will...",
|
||||
"pubDate": "Wed, 09 Sep 2020 19:30:01 +0000",
|
||||
"author": {
|
||||
"name": "Julian Lam",
|
||||
"url": "https://community.nodebb.org/user/julian",
|
||||
"image": "https://community.nodebb.org/assets/uploads/profile/uid-2/2-profileavatar-1738544541106.jpeg"
|
||||
},
|
||||
"content": "forums-and-the-new-era-of-elearning.html"
|
||||
},
|
||||
{
|
||||
"url": "https://nodebb.org/blog/nodebb-v1-14-3-a-critical-security-update",
|
||||
"title": "NodeBB v1.14.3: A Critical Security Update",
|
||||
"excerpt": "A bug in our validation logic made it possible to change the password of any user on a running NodeBB forum by sending a specially crafted socket.io call to the...",
|
||||
"pubDate": "Mon, 17 Aug 2020 22:41:27 +0000",
|
||||
"author": {
|
||||
"name": "Andrew Rodrigues",
|
||||
"url": "https://community.nodebb.org/user/psychobunny",
|
||||
"image": "https://avatars.githubusercontent.com/u/654670?v=4"
|
||||
},
|
||||
"content": "nodebb-v1-14-3-a-critical-security-update.html"
|
||||
},
|
||||
{
|
||||
"url": "https://nodebb.org/blog/a-decade-in-the-industry-a-ten-year-retrospective",
|
||||
"title": "A decade in the industry; a ten year retrospective",
|
||||
"excerpt": "Recently, a milestone had passed me by without my knowing – the original start date for my very first programming job was May 3rd, 2010. Ten years have flown by...",
|
||||
"pubDate": "Sat, 15 Aug 2020 21:39:07 +0000",
|
||||
"cover": "https://nodebb.org/blog/images/a-decade-in-the-industry-a-ten-year-retrospective.jpg",
|
||||
"author": {
|
||||
"name": "Julian Lam",
|
||||
"url": "https://community.nodebb.org/user/julian",
|
||||
"image": "https://community.nodebb.org/assets/uploads/profile/uid-2/2-profileavatar-1738544541106.jpeg"
|
||||
},
|
||||
"content": "a-decade-in-the-industry-a-ten-year-retrospective.html"
|
||||
},
|
||||
{
|
||||
"url": "https://nodebb.org/blog/the-faceless-master",
|
||||
"title": "The Faceless Master",
|
||||
"excerpt": "This past month, my family asked me to design a website to showcase my late grandfather’s paintings. Grandpa passed away in ’93, the same year that I was born. We...",
|
||||
"pubDate": "Mon, 27 Jul 2020 17:08:30 +0000",
|
||||
"cover": "https://images.unsplash.com/photo-1496631488200-c0b85f3044a7?q=80&w=1740&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
"author": {
|
||||
"name": "Nick Weiner",
|
||||
"url": "https://community.nodebb.org/user/signal_rorak",
|
||||
"image": "https://community.nodebb.org/assets/uploads/profile/uid-2/2-profileavatar-1738544541106.jpeg"
|
||||
},
|
||||
"content": "the-faceless-master.html"
|
||||
},
|
||||
{
|
||||
"url": "https://nodebb.org/blog/nodebb-1-14-0-distance-wont-keep-us-from-moving-forward",
|
||||
"title": "NodeBB 1.14.0 – Distance Won’t Keep Us From Moving Forward",
|
||||
"excerpt": "It’s been several months since our Toronto team has convened in our downtown office, and we’ve blogged previously about how we are spending our free time during social distancing. But...",
|
||||
"pubDate": "Fri, 03 Jul 2020 15:18:01 +0000",
|
||||
"cover": "https://nodebb.org/blog/images/nodebb-1-14-0-distance-wont-keep-us-from-moving-forward.jpg",
|
||||
"author": {
|
||||
"name": "Jay Moonah",
|
||||
"url": "https://community.nodebb.org/user/jay-moonah",
|
||||
"image": "https://community.nodebb.org/assets/uploads/profile/uid-7950/7950-profileavatar-1701887954691.jpeg"
|
||||
},
|
||||
"content": "nodebb-1-14-0-distance-wont-keep-us-from-moving-forward.html"
|
||||
},
|
||||
{
|
||||
"url": "https://nodebb.org/blog/digitally-overwhelmed-podcast-episode-140",
|
||||
"title": "Digitally Overwhelmed Podcast, Episode 140",
|
||||
"excerpt": "I had the opportunity to speak on the topic of forum software and its role in the modern web, with my good friend Cinthia Pacheco of Digital Bloom IQ. Cinthia...",
|
||||
"pubDate": "Fri, 19 Jun 2020 14:00:00 +0000",
|
||||
"cover": "https://nodebb.org/blog/images/digitally-overwhelmed-podcast-episode-140.jpg",
|
||||
"author": {
|
||||
"name": "Julian Lam",
|
||||
"url": "https://community.nodebb.org/user/julian",
|
||||
"image": "https://community.nodebb.org/assets/uploads/profile/uid-2/2-profileavatar-1738544541106.jpeg"
|
||||
},
|
||||
"content": "digitally-overwhelmed-podcast-episode-140.html"
|
||||
},
|
||||
{
|
||||
"url": "https://nodebb.org/blog/unveiling-of-the-read-api",
|
||||
"title": "Unveiling of the Read API",
|
||||
"excerpt": "Developer empowerment has always been at the core of NodeBB: Plugins receive first-class treatment, in that there are plenty of hooks enabling plugins to interact with nearly all facets of...",
|
||||
"pubDate": "Mon, 01 Jun 2020 16:32:09 +0000",
|
||||
"cover": "https://images.unsplash.com/photo-1544716278-e513176f20b5?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
"author": {
|
||||
"name": "Julian Lam",
|
||||
"url": "https://community.nodebb.org/user/julian",
|
||||
"image": "https://community.nodebb.org/assets/uploads/profile/uid-2/2-profileavatar-1738544541106.jpeg"
|
||||
},
|
||||
"content": "unveiling-of-the-read-api.html"
|
||||
}
|
||||
|
||||
]
|
|
@ -47,6 +47,7 @@ const defaultCovers = [
|
|||
'https://images.unsplash.com/photo-1625297671662-f073f2a91528?q=80&w=1740&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
|
||||
'https://images.unsplash.com/photo-1742198810079-49bb51d1c5af?q=80&w=1969&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
|
||||
'https://images.unsplash.com/photo-1572044162444-ad60f128bdea?q=80&w=1740&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
|
||||
'https://images.unsplash.com/photo-1519389950473-47ba0277781c?q=80&w=1740&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
|
||||
];
|
||||
let defaultCoverIndex = 0;
|
||||
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
<div class="kg-card-markdown">
|
||||
<p>Recently, a milestone had passed me by without my knowing – the original start date for my very first programming job was May 3<sup>rd</sup>, 2010. Ten years have flown by (or in some cases, have dragged by), and much has changed both in my personal and professional life, and much has stayed the same. I thought it would be a nice exercise to look back and see where I came from.</p>
|
||||
|
||||
<p>That first job was for a PHP Intern position at a now defunct company called <a href="https://www.crunchbase.com/organization/social-game-universe">Social Game Universe</a>. By day, this company made Facebook games, and did consulting for clients looking to get their games made, but SGU (as we called it then) also developed a platform for games to hook into, allowing for actions and interactions <em>between</em> apps/games, which was a novel idea at the time.</p>
|
||||
|
||||
<p>At the time, I believed that we were first-to-market, but we ended up getting beat out by <a href="https://parseplatform.org/">Parse</a>, which completely upended the industry and once Facebook bought out parse, it spelled the end of that particular project's potential.</p>
|
||||
|
||||
<p>However, <strong>more importantly</strong>, SGU was where I first met <a href="https://blog.nodebb.org/meet-the-leadership-team/">Andrew and Baris</a>. Andrew had joined the team around the same time, and Baris, then new to Canada, had actually joined Bitcasters, another company working in the same office, before it was later folded into Social Game Universe.</p>
|
||||
|
||||
<p>I will always look back fondly on this first job, as it provided me with a safe a welcoming space (when it wasn't crunch time, of course) to learn javascript and experiment with new ideas. Many of the lessons I learned at SGU I later put into practice at my subsequent jobs, including at NodeBB.</p>
|
||||
|
||||
<h2 id="stateoftheindustry">State of the industry</h2>
|
||||
<p>2010 was only a couple years past the DHTML craze of the early aughts. Javascript was very much still considered a "toy" language, but developers were starting to embrace it for website enhancements (think XHR/AJAX for dynamically loading content, etc.)</p>
|
||||
|
||||
<p>Some notable trends – YUI and Dojo were going strong, but faltering to newer entrants like Mootools and jQuery (spoiler alert, only one of those names is recognized now...).</p>
|
||||
|
||||
<p>Mootools ended up losing to jQuery, but will always be remembered as the reason why we don't have <code>Array.contains()</code>, but instead use <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes"><code>Array.includes()</code></a>. Mootools had no qualms about extending the prototype, and was popular enough at the time that its <code>Array.contains()</code> method would conflict badly, if one were provided by the browser, and so <code>Array.includes()</code> it was... after all, one doesn't simply break the web!</p>
|
||||
|
||||
<p>I remember betting on Mootools, but just like HD-DVD and Blu-ray, eventually the other side won out ?</p>
|
||||
|
||||
<p>While I was at that first job, I also had the bright idea to load page html via AJAX, isntead of doing a whole page load, thereby saving on loading all of the page boilerplate. Little did I know, I re-invented the concept of the single-page application. <a href="https://en.wikipedia.org/wiki/Convergent_evolution">Talk about convergent evolution!</a></p>
|
||||
|
||||
<h3 id="ohdidimentionthatthisjobwasunpaid"><strong>Oh, did I mention that this job was UNPAID?!</strong></h3>
|
||||
<p>At the time, the gaming industry was exploding. Games like Farmville and Words with Friends were capturing huge audiences, and many like myself looked at these smaller gaming studios as a way into the industry, hopefully landing at one of the triple A game studios later on.</p>
|
||||
|
||||
<p>Everybody wanted in, and so conditions were ripe for exploiting programmers for low or no pay!</p>
|
||||
|
||||
<h2 id="angryyoungmen">Angry young men...</h2>
|
||||
<p>Back then, we were naive, with ambitious ideas that we wanted to execute ASAP. Looking back, stifling this behaviour would have been the worst thing to do. Nurturing and exploring this aspect of our youth would be the best gift you could give to someone learning. We often poke fun at this internally, with the phrase "angry young men", functioning to remind us of how we were often quick to anger and always thought we knew what the best practice ought to be.</p>
|
||||
|
||||
<p>Over the years, I personally have learned a lot, including (but of course, not limited to):</p>
|
||||
<ul>
|
||||
<li>Leadership and management</li>
|
||||
<li>Business development</li>
|
||||
<li>Best practices for programming (duh)</li>
|
||||
<li>Soft skills such as conflict resolution</li>
|
||||
<li>Organisational skills, and project coordination</li>
|
||||
</ul>
|
||||
<p>There is much more for me to learn, and I can't wait to see how I will continue to grow as a developer in the next 10 years.</p>
|
||||
|
||||
<h2 id="wheredidyouseeyourselfbeingin10yearstime">Where did you see yourself being in 10 years' time?</h2>
|
||||
<p>Mark Zuckerberg once opined that <a href="https://www.cnet.com/news/say-what-young-people-are-just-smarter/">"young people are just smarter"</a>. While in many ways that can be true, I can say without hesitation that I am a better programmer than I was 10 years ago.</p>
|
||||
|
||||
<p>My one takeaway would be:</p>
|
||||
<blockquote>If you look back at your old code and are proud of it, then that means <strong>you have not grown as a developer</strong>.</blockquote>
|
||||
<p>There is merit in youthful talent, and nobody works harder than someone with something to prove, but age and experience teaches you to work smarter, not harder.</p>
|
||||
|
||||
<p>Nothing put this into sharp relief quite like having a baby, combined with the coronavirus pandemic. My son, Zachary, was born September 2019, and with it disappeared much of my free time that I had taken for granted. I finally understood what it meant to "make time" for something, because there was literally no more time in the day that wasn't better spent doing something else. "Down time" no longer existed. The coronavirus pandemic later threw the world for a loop in terms of working from home/adjusted work hours, which led to another revelation: while I was getting fewer hours in at my desk, I was working much more efficiently during those hours, and I would spend less time banging my head against non-working code, simply because I couldn't afford to do so (if I have no childcare, my day is essentially broken up into chunks that correspond with his naps.)</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<p>10 years ago, if you had asked me where I would see myself today, I probably would have answered with a variation of the following:</p>
|
||||
<ul>
|
||||
<li>Working at a large, pubicly traded corporation</li>
|
||||
<li>Job safety was my main concern at that time, I firmly believed in a steady paycheque</li>
|
||||
<li>Perhaps moving into management, despite my passion for solving problems and creating interesting projects</li>
|
||||
<li>Ideally in a government job, working for the city, etc.</li>
|
||||
</ul>
|
||||
|
||||
<p>I would never have imagined that I would've made the jump to entrepreneurship, the antithesis of a steady paycheque! I'll always be grateful to Andrew and Baris for pushing me towards starting up our own company (Design Create Play in 2013, and NodeBB in 2014). The rest, as they say, is history.</p>
|
||||
|
||||
<h2 id="whatsoursecret">What's our secret?</h2>
|
||||
|
||||
<p>Slow and steady wins the race. We didn't want to be a flash in the pan, in those early days, profitability was our main goal.</p>
|
||||
|
||||
<p>Chasing the hockey stick is sexy, but is by no means a guarantee.</p>
|
||||
|
||||
<p>Take time often to stop, re-evaluate, reposition, and continue upwards.</p>
|
||||
|
||||
<p>Here's to a decade in the industry, and here's to 10 more.</p>
|
||||
|
||||
</div>
|
|
@ -0,0 +1,18 @@
|
|||
<div class="kg-card-markdown">
|
||||
|
||||
I had the opportunity to speak on the topic of forum software and its role in the modern web, with my good friend Cinthia Pacheco of <a href="https://digitalbloomiq.com/">Digital Bloom IQ</a>. Cinthia and I go way back, to our high school days when we were hacking away (in the most non-criminal sense of the word, of course) at visual basic in our Computer Sciences class.
|
||||
|
||||
In a modern web dominated by social media giants like Facebook and Twitter, how does a forum solution fit in with an existing brand strategy? How can companies both small and large harness the benefits of community-building without adding <em>yet another tool</em> to manage and maintain?
|
||||
|
||||
Cinthia and I speak on these topics and more in...
|
||||
|
||||
?? <a href="http://digitalbloomiq.com/pod/nodebb"><strong>Episode 140 of the Digitally Overwhelmed podcast</strong></a> ??
|
||||
|
||||
<hr />
|
||||
|
||||
<h3 id="moreaboutcinthia">More about Cinthia...</h3>
|
||||
<img style="float: right; margin: 0 0 1.5em 1.5em;" src="https://nodebb.wpcomstaging.com/wp-content/uploads/2021/08/digitally-overwhelmed-podcast-episode-140.jpg" alt="Digitally Overwhelmed Podcast, Episode 140" /> Cinthia is owner/founder of Digital Bloom IQ and is passionate about helping Health and Wellness businesses heal more of the world through SEO (Search Engine Optimization). After four years of corporate experience working with companies like Avon, Sears, and Hyundai, she transitioned into the small business world, focusing on her SEO and Google Analytics services. She is on a mission to inspire Health and Wellness business to be more intentional about their SEO marketing and share more of the healing talents.
|
||||
|
||||
When she’s not working, you can find her hanging out with her cats, dogs, & boyfriend outside or journaling on her couch.
|
||||
|
||||
</div>
|
40
blog_source/posts/forums-and-the-new-era-of-elearning.html
Normal file
|
@ -0,0 +1,40 @@
|
|||
<div class="kg-card-markdown">
|
||||
|
||||
<p>The ongoing coronavirus pandemic has changed the way we view eLearning. With students preparing to go back to school, parents, teachers, and students alike are wondering how this year will shape up.</p>
|
||||
|
||||
<p>In Ontario, Doug Ford's conversative government had already made strides towards remote eLearning even before the first case of COVID-19 was diagnosed in China. Of course, the priorities then were all about finding efficiencies in government, and while trimming the fat will always be popular with a conservative govenrment, it is at least <em>a little</em> ironic that likely <strong>more</strong> money is now being spent ensuring that students can proceed with this coming year uninterrupted.</p>
|
||||
|
||||
<p>As cities, provinces, and countries re-open, more attention is being given to academic, and how entire cohorts of students must be accommodated in this new era.</p>
|
||||
|
||||
<p>George Veletsianos, a professor in the school of education and technology at Royal Roads University and Canada Research Chair in Innovative Learning and Technology, had this to say about the differences between in-person and eLearning strategies:</p>
|
||||
<blockquote>the meta-analyses have found outcomes between the two are generally the same. If there’s any sort of difference, it tends to favour blended courses.</blockquote>
|
||||
|
||||
<p>As aptly put by Macleans' staff writer Stacy Lee Kong:</p>
|
||||
|
||||
<blockquote>The recent pivot to remote learning during the pandemic was ad hoc, inconsistent and happened during a time of great emotional upheaval for students</blockquote>
|
||||
<p>(The article about this, <a href="https://www.macleans.ca/education/why-learning-from-home-is-an-unlikely-training-ground-for-a-post-pandemic-world/">you can find here on the Macleans website</a>)</p>
|
||||
|
||||
<p>It's no surprise that academic institutions everywhere are looking for best practices in a field that has had much less development, relative to in-person teaching.</p>
|
||||
|
||||
<p>However, there have been many success stories related to online education, such as Khan Academy and Udemy. It is clearly a viable way to teach, and these services arguably become more valuable the longer we're in the throes of this pandemic.</p>
|
||||
|
||||
<p>I had the opportunity to talk to <a href="https://www.itpro.tv/edutainers/">Don Pezet, CEO of ITProTV</a>, about how they are handling the coronavirus pandemic, and asked him to share his thoughts on eLearning at large.</p>
|
||||
|
||||
<strong>If you were to give one piece of advice to academic institutions looking to quickly adopt to remote learning, what would it be?</strong>
|
||||
|
||||
<blockquote>The key to remote learning is maintaining a "presence" with your learners. They have an expectation of being able to see and interact with their teachers. Your online platform needs to replicate this as closely as possible, while remaining accessible to all your students. Synchronous video conferencing is the closest reproduction of the classroom experience, but it is not realistic for many organizations especially when dealing with underprivileged students. Online chat and forum platforms are an ideal middle ground where you can directly interact with your students without needing special equipment, large amounts of bandwidth, or scheduled meeting times.</blockquote>
|
||||
<strong>What pitfalls would you caution against as academic institutions try to implement something quickly in time for September?</strong>
|
||||
<blockquote>The biggest pitfall is trying to do too much all at once. This leads to a lot of complexity for the teachers and students who end up spending more time struggling with the platform instead of learning. Start out small. Establish the minimum viable product needed to educate your students. Then, as people settle in you can start to integrate more advanced technologies like synchronous video.</blockquote>
|
||||
<strong>ITPro.TV combines video-based lectures with a forum component for follow-up questions and collaboration. Do you feel this model works well compared to dedicates courseware solutions?</strong>
|
||||
<blockquote>We cater to audiences around the globe. It is easy for learners in the Americas to tune in to watch our training live and ask our Edutainers questions right there on the spot. However, that leaves out two big demographics: Everyone outside of the Americas, and everyone watching our previously recorded training. We want all of our learners to have a chance to interact with an Edutainer to get their questions answered and help build a personal relationship between the teacher and the student. Our forums directly support that. People all over the world, in any time zone, can post questions and have conversations not only with our Edutainers, but with other people who are studying the same material. This helps to build a community and create a support structure for each person as they study.</blockquote>
|
||||
|
||||
<hr />
|
||||
|
||||
<p>Thanks again to Don for providing such valuable insight!</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<ul>
|
||||
<li>Photo by <a href="https://unsplash.com/@marvelous?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Marvin Meyer</a> on <a href="https://unsplash.com/s/photos/digital?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></li>
|
||||
</ul>
|
||||
</div>
|
|
@ -0,0 +1,34 @@
|
|||
<div class="kg-card-markdown">
|
||||
|
||||
It's been several months since our Toronto team has convened in our downtown office, and we've blogged previously about how we are <a href="https://blog.nodebb.org/social-distancing-whats-that/">spending our free time during social distancing</a>. But of course that DOES NOT mean the team hasn't been working hard. Version 1.14.0 includes expanded documentation and improved features, as well as streamlining and security fixes. Here are some highlights:
|
||||
<h2 id="readapidocs">Read API Docs</h2>
|
||||
Since the begining, NodeBB has had a powerful Read API which allows for the JSON for almost every route to be be shown by simply appending "/api" into the path (e.g. the code behind the most recent posts on our own forum can be see at <a href="https://community.nodebb.org/api/recent">https://community.nodebb.org/api/recent</a>). A <a href="https://blog.nodebb.org/unveiling-of-the-read-api/">previous blog post talked in depth about the work being done to document this better</a>, and you can now find a proper list of these routes at:
|
||||
|
||||
<a href="https://docs.nodebb.org/api/">https://docs.nodebb.org/api/</a>
|
||||
<h2 id="moderationimprovements">Moderation Improvements</h2>
|
||||
The 1.14.0 includes a number of improvements to moderation tasks, including better tools for moderators and admins as well as better messaging for users. These include:
|
||||
<ul>
|
||||
<li>New moderation options on the flags detail page: quick assign, ban user, and delete user</li>
|
||||
<li>Better messaging for account deletion (i.e. deletion via account page only deletes account, not content)</li>
|
||||
<li>Admins are now able to choose between deletion of account, content, or both, in account page and ACP.</li>
|
||||
<li>An improved dialog for topic merging, allowing for searching of topics and choosing which topic to merge into</li>
|
||||
<li>Post history can now be restored at the press of a button -- a restored version is just a new edit, so history is preserved</li>
|
||||
</ul>
|
||||
<h2 id="morepowerinprivileges">More Power in Privileges</h2>
|
||||
NodeBB's privileges system has allowed for individual users or groups to be given a wide variety of reading, posting and moderation abilities globally or within specific categories of a forum. With the latest release, admins are now able to grant more fine-grained access control to the Admin Control Panel, including the dashboard, category management, privileges, user management, and forum settings
|
||||
<ul>
|
||||
<li>Privilege grant/rescind now fires plugin hooks for all types, category, global, and admin hooks</li>
|
||||
</ul>
|
||||
<h2 id="otherchanges">Other Changes</h2>
|
||||
<ul>
|
||||
<li>Edits to posts inside watched topics now send notifications</li>
|
||||
<li>User blocks functionality now has plugin hooks</li>
|
||||
<li>Profile export now returns more user data</li>
|
||||
<li>Grunt restart/rebuild and filechange detection have been sped up</li>
|
||||
<li>The General menu has been removed from the Admin Control Panel, with items there mostly moved to Settings</li>
|
||||
</ul>
|
||||
Because this release contains a number of security fixes, we <strong>highly recommend upgrading at your earliest convenience</strong> If you are running your own server and encounter any issues upgrading, please post them in the <a href="https://community.nodebb.org/">community support forum</a>.
|
||||
|
||||
If you are a hosting client of with us, please drop a line to <a href="mailto:support@nodebb.org">support@nodebb.org</a> to schedule a time for our team to perform the upgrade.
|
||||
|
||||
</div>
|
|
@ -0,0 +1,27 @@
|
|||
<div class="kg-card-markdown">
|
||||
|
||||
<p>Unfortunately, after only returning a couple of times to our Toronto office, the city started to see a new spike in COVID-19 cases, so we're back to working remotely. But that certainly won't stop us from continuing to improve NodeBB, including fast-tracking a long-planned feature.</p>
|
||||
|
||||
<h2 id="writeapimergedintocore">Write API Merged Into Core</h2>
|
||||
This was originally meant to be a part of version 2.0, but instead we decided to move ahead and bring it out even sooner. For those who had previously been using the Write API plug-in, it should be a simple switch from the <code>v2</code> endpoint to the new <code>v3</code> endpoint, although it is wise to <a href="https://github.com/NodeBB/NodeBB/pull/8708#issuecomment-705780711">check the breaking changes</a> to see if you are affected. Note that you <em>can</em> continue to use the plugin as it will not conflict with the new API. Eventually we will expand this API with <code>GET</code> routes and make it a complete API, but for now retrieving data from NodeBB can still be done by prepending <code>/api</code> to all page routes. (As a reminder, we recently released a list of all routes at <a href="https://docs.nodebb.org/api/">https://docs.nodebb.org/api/</a>).
|
||||
<h2 id="flagsimprovements">Flags Improvements</h2>
|
||||
The previous release included a number of moderation improvements, and we've continued the work on flags to make it more usable:
|
||||
<ul>
|
||||
<li>Multiple flag reports are now consolidated into a single flag, so duplicate reports for the same piece of content are now simply added to the existing flag rather than generating multiple reports</li>
|
||||
<li>Flags page has been updated to allow for additional functionality (sorting, and bulk actions)</li>
|
||||
</ul>
|
||||
<h2 id="otherchanges">Other Changes</h2>
|
||||
<ul>
|
||||
<li>Progressive Web Application enhancements – NodeBB is now "installable" from mobile devices to your home screen, and should act like an app now
|
||||
** This is one of the starting points to eventually allowing NodeBB to be accessible while offline, just like a native app</li>
|
||||
<li>"Verified Users" is now a separate system group, allowing you to segment privileges and control category access to those specific users
|
||||
** Verified users are those users who have confirmed (or "verified") their email address</li>
|
||||
<li>Able to search a particular categories including its children
|
||||
** Before, you had to select all of the categories, including its children</li>
|
||||
<li>And finally because we're always thinking about performance, Baris Usakli has made a number of optimizations that should allow NodeBB to handle more traffic</li>
|
||||
</ul>
|
||||
Be sure to check out the breaking changes at <a href="https://community.nodebb.org/topic/14917/1-15-0-breaking-changes">https://community.nodebb.org/topic/14917/1-15-0-breaking-changes</a>. As always we <strong>recommend backing up your database, and upgrading at your earliest convenience</strong>. If you are running your own server and encounter any issues, please drop a post on the <a href="https://community.nodebb.org/">community support forum</a>.
|
||||
|
||||
If you are a hosting client of with us, please drop a line to <a href="mailto:support@nodebb.org">support@nodebb.org</a> to schedule a time for our team to perform the upgrade.
|
||||
|
||||
</div>
|
|
@ -0,0 +1,6 @@
|
|||
<div class="kg-card-markdown"><p>A bug in our validation logic made it possible to change the password of any user on a running NodeBB forum by sending a specially crafted socket.io call to the server.</p>
|
||||
<p>We have resolved this in the latest version of NodeBB, and the fix has already been rolled out as a patch on all of our hosted customers.</p>
|
||||
<p>For more information on the vulnerability as well as instructions on how to resolve this issue, please have a look here: <a href="https://github.com/NodeBB/NodeBB/security/advisories/GHSA-hr66-c8pg-5mg7">https://github.com/NodeBB/NodeBB/security/advisories/GHSA-hr66-c8pg-5mg7</a></p>
|
||||
<p>If you are unable to upgrade ASAP, you can also apply the patch via cherry-picking <a href="https://github.com/NodeBB/NodeBB/commit/16cee1b03ba3eee177834a1fdac4aa8a12b39d2a">this commit</a>.</p>
|
||||
<p>As this release contains a critical security fix, we <strong>highly recommend upgrading at your earliest convenience</strong>. If you are running your own server and encounter any issues upgrading, please post them in the <a href="https://community.nodebb.org/">community support forum</a>.</p>
|
||||
</div>
|
|
@ -0,0 +1,24 @@
|
|||
<div class="kg-card-markdown">
|
||||
|
||||
<p>It's not news to say 2020 has been... challenging. In Toronto, the home of NodeBB HQ, we've gone from a spring lockdown to a cautious summer reopening, to lockdown again — <em>sigh</em>. But the team has never stopped working, and have managed to squeeze out one last release before we thankfully turn over the calendar.</p>
|
||||
|
||||
<p>Improvements include:</p>
|
||||
<ul>
|
||||
<li><strong>Topic thumbnails.</strong> Multiple topic thumbnails now supported per topic. Theme integration (e.g. Persona) will follow in the near future.</li>
|
||||
</ul>
|
||||
<img src="https://nodebb.wpcomstaging.com/wp-content/uploads/2021/08/nodebb-v1-16-0-one-last-release-for-a-weird-year.png" alt="NodeBB v1.16.0 - One Last Release For A Weird Year" />
|
||||
<ul>
|
||||
<li><strong>Plugins admin.</strong> Plugins are now able to override the ACP relogin challenge. The default behaviour was (and still remains) a re-entering of the password. As an example, the <a href="https://github.com/julianlam/nodebb-plugin-2factor/">Two-Factor Authentication plugin</a> takes advantage of this new functionality by presenting a 2FA challenge instead.</li>
|
||||
<li><strong>Updated topic navigator.</strong> We have added a new topic navigator for those using infinite scrolling! Click the bar at the bottom right to open the navigator and try it out for yourself.</li>
|
||||
</ul>
|
||||
<img src="https://nodebb.wpcomstaging.com/wp-content/uploads/2021/08/nodebb-v1-16-0-one-last-release-for-a-weird-year.gif" alt="NodeBB v1.16.0 - One Last Release For A Weird Year" />
|
||||
|
||||
<p>For a list of breaking changes, please see:</p>
|
||||
|
||||
<a href="https://community.nodebb.org/topic/15178/1-16-0-breaking-changes">https://community.nodebb.org/topic/15178/1-16-0-breaking-changes</a>
|
||||
|
||||
<p>As always if you are hosting your own NodeBB forum we recommend <a href="https://docs.nodebb.org/configuring/upgrade/">updating to the latest version</a> to take advantage of the latest features and fixes. Remember to back up your database before proceeding with any upgrade.</p>
|
||||
|
||||
<p>If you encounter any issues, please post them in our community support forum.</p>
|
||||
|
||||
</div>
|
|
@ -0,0 +1,51 @@
|
|||
<div class="kg-card-markdown">
|
||||
|
||||
<p>Spring has sprung in Toronto, so we're taking advantage by getting some <em>extremely</em> low budget advertising. Hey, doesn't everyone make their software purchasing decisions based on what they read on the sidewalk? Hah-hah!</p>
|
||||
|
||||
<p>Onward with our latest release, NodeBB version 1.17.0. Improvements include:</p>
|
||||
<ul>
|
||||
<li><strong>Scheduled Topics</strong> We've had multiple requests for this one. Rather than simply submitting a topic immediately users now have the option to select a time and date for it to go live. If you wish to limit this ability to certain users, there's also a new category-level privilege that can toggled for individuals or user groups.</li>
|
||||
<li><strong>Post Diff Deletion</strong> Specific entries in post history can now be removed. This resolved an issue where simply editing a post to remove sensitive content, such as passwords or secret key was not enough, as the post history would still allow you to easily reconstruct an old post.</li>
|
||||
<li><strong>Topic Event Improvements</strong> Topic events such as pinning or locking are now displayed in-line with posts, and plugins can add their own topic events. Previously, topic actions were done in the background, so this increases the transparency of moderation actions.</li>
|
||||
<li><strong>Categories System Refactoring</strong> If your forum only has a few categories then loading all of them is probably not an issue, but increasingly some larger NodeBB forums have hundreds or even thousands of categories, which can cause performance issues. Now the routes that were previously loading all categories can be paginated via a setting in the admin control panel, reducing the load considerably.</li>
|
||||
<li><strong>Filter Tags by Topic</strong> We've added the ability to filter the tags page by category, so users can more easily filter out tags that might not be relevant to what they are looking for.</li>
|
||||
<li><strong>Timeline Design added to Persona Theme</strong> This change to our most popular theme makes it easier to follow conversations, particularly long running ones:
|
||||
<img src="https://nodebb.wpcomstaging.com/wp-content/uploads/2021/08/nodebb-v1-17-0-scheduled-topics-new-moderation-features-and-more.png" alt="NodeBB v1.17.0 - Scheduled Topics, New Moderation Features and More" /></li>
|
||||
<li><strong>Developer Improvements</strong>
|
||||
<ul>
|
||||
<li>Re-mountable routes
|
||||
<ul>
|
||||
<li>It is now possible to (via custom plugin logic) override specific mountpoints in NodeBB. For example, instead of /category (and related pages such as /category/5/category-name, the mount point can be renamed to /kategori for German communities</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Sorted lists available on client side
|
||||
<ul>
|
||||
<li>The "sorted-lists" functionality was primarily an ACP-only library. We now enabled this to be used on the front-end.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Client-side hooks
|
||||
<ul>
|
||||
<li>Now filter, static, and action hooks are available on client-side.</li>
|
||||
<li>A new "hooks" module has been added to v1.17.0, allowing the use of filter, static, and action hooks similar to the server-side. Prior to this, we were limited to <code>$(window).trigger</code> calls, which had limitations, especially for asynchronous tasks. (see <a href="https://github.com/NodeBB/NodeBB/blob/32c20806bc8467953ba354042b870ed8794f8517/public/src/admin/admin.js#L33-L38#L33-L38">https://github.com/NodeBB/NodeBB/blob/32c20806bc8467953ba354042b870ed8794f8517/public/src/admin/admin.js#L33-L38#L33-L38</a> for details)</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Security Fixes
|
||||
<ul>
|
||||
<li>One XSS vulnerability fixed</li>
|
||||
<li>One security best practice implemented, to guard against session fixation attacks</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<p>For a breaking changes, please see:</p>
|
||||
|
||||
<a href="https://community.nodebb.org/topic/15259/1-17-0-breaking-changes">https://community.nodebb.org/topic/15259/1-17-0-breaking-changes</a>
|
||||
|
||||
<p>As usual if you are hosting your own NodeBB forum we highly recommend <a href="https://docs.nodebb.org/configuring/upgrade/">updating to the latest version</a> to take advantage of the latest features and fixes. Always remember to back up your database before proceeding with any upgrade.</p>
|
||||
|
||||
<p>If you encounter any issues, please post them on our community support forum.</p>
|
||||
|
||||
</div>
|
213
blog_source/posts/optimizing-benchpress.html
Normal file
|
@ -0,0 +1,213 @@
|
|||
<div class="kg-card-markdown"><h1 id="optimizingbenchpress">Optimizing Benchpress</h1>
|
||||
<p>Recently, I saw the release of <a href="https://github.com/Geal/nom/">nom v6</a> and decided I wanted to try it out, and see if I could speed up my hobby JS template compiler, <a href="https://github.com/benchpressjs/benchpressjs">BenchpressJS</a>.</p>
|
||||
<h2 id="background">Background</h2>
|
||||
<p>Benchpress is a template compiler and tiny runtime which is focused on two things:</p>
|
||||
<ul>
|
||||
<li>Backwards compatibility with <a href="https://github.com/benchpressjs/benchpressjs/tree/templates.js-legacy">templates.js</a> syntax</li>
|
||||
<li>Runtime speed</li>
|
||||
<li>(As of 2018) Compilation speed</li>
|
||||
</ul>
|
||||
<p>A short history:</p>
|
||||
<ul>
|
||||
<li>2014: templates.js is created as a light library for use in NodeBB.<br>
|
||||
It used Regular Expressions to fill in template data at runtime, allowing for an unconventional syntax. No precompilation is necessary or possible.</li>
|
||||
<li>2017: Benchpress is created as a backwards-compatible replacement for templates.js<br>
|
||||
It provides much faster runtime performance by compiling templates into Javascript code.</li>
|
||||
<li>2018: A compiler rewrite is undertaken utilizing Rust, as the JS compiler (based on Regexp) is quite slow.<br>
|
||||
This version of the compiler, named <a href="https://github.com/benchpressjs/benchpress-rs"><strong>benchpress-rs</strong></a>, uses <a href="https://neon-bindings.com/">Neon</a> to interface with Node, and is about 30x faster than the JS version.<br>
|
||||
The JS compiler continues to ship alongside, since pre-built binaries are not available for all platforms.</li>
|
||||
<li>Nov 7, 2020: Realizing WASM module support is available on all supported version of Node, the JS compiler is removed, and the Rust compiler is shipped solo as a WASM module. Having a single codebase opens up the door to adding more features, but benchpress-rs is pretty hacky and difficult to work on.</li>
|
||||
<li>Nov 15, 2020: Another compiler rewrite is finished, using the <strong>nom</strong> parser combinator library.<br>
|
||||
This rewrite is optimized to be 4.4x as fast as the previous version, on top of being far more maintainable and extensible.<br>
|
||||
<em>I'd say a week is pretty good, all things considered</em></li>
|
||||
</ul>
|
||||
<h2 id="thedrop">The Drop</h2>
|
||||
<p>Much of the time implementing the rewrite was getting my new version to pass the <a href="https://github.com/benchpressjs/benchpressjs/tree/master/tests/templates/source">quite extensive suite of integration tests</a> (especially getting Spans working), but that's not what I'm here to talk about. I'm here to talk about optimizing it after all of that, because I have a shameful secret:</p>
|
||||
<blockquote>
|
||||
<p>It was <em>slower</em>.</p>
|
||||
</blockquote>
|
||||
<p>Yep, after all of that work, the end result was actually slower than the previous version. Let's see the benchmarks.</p>
|
||||
<h4 id="benchmarksetup">Benchmark Setup</h4>
|
||||
<p>I set up benchmarks both in Rust and Node. Both are based on two large template files: <code>categories.tpl</code> and <code>topic.tpl</code>, which are from some NodeBB at some point I think.</p>
|
||||
<p>Essentially, each benchmark compiles these repeatedly. Here's what I saw:</p>
|
||||
<p><strong>Before</strong></p>
|
||||
<pre><code class="language-sh">$ cargo +nightly bench
|
||||
running 2 tests
|
||||
test bench_compile_categories ... bench: 329,287 ns/iter (+/- 120,222)
|
||||
test bench_compile_topic ... bench: 4,402,767 ns/iter (+/- 63,216) test result: ok. 0 passed; 0 failed; 0 ignored; 2 measured; 0 filtered out $ grunt bench 2>/dev/null
|
||||
Running "benchmark" task
|
||||
compilation x 74.70 ops/sec ±0.32% (167 runs sampled) Done.
|
||||
</code></pre>
|
||||
<p><strong>After</strong></p>
|
||||
<pre><code class="language-sh">$ cargo +nightly bench
|
||||
running 2 tests
|
||||
test bench_compile_categories ... bench: 2,498,374 ns/iter (+/- 11,032)
|
||||
test bench_compile_topic ... bench: 15,065,914 ns/iter (+/- 51,794) test result: ok. 0 passed; 0 failed; 0 ignored; 2 measured; 0 filtered out $ grunt bench 2>/dev/null
|
||||
Running "benchmark" task
|
||||
compilation x 33.88 ops/sec ±0.26% (175 runs sampled) Done.
|
||||
</code></pre>
|
||||
<p>Woops! That's more than twice as slow. And even worse, the vast majority of that time is spent in Rust.</p>
|
||||
<h4 id="flamegraphing">Flamegraphing</h4>
|
||||
<p>One of the best tools, in my opinion, for finding optimization opportunities is the flamegraph. This neat little figure will show you where the time is spent in your program. So, let's make one with the very handy <a href="https://github.com/flamegraph-rs/flamegraph">cargo flamegraph tool</a>:</p>
|
||||
<pre><code class="language-sh">$ cargo +nightly flamegraph --bin bench # bench refers to a binary based on the benchmarks run before
|
||||
</code></pre>
|
||||
<p><a href="https://blog.nodebb.org/content/images/2020/11/flamegraph_before.svg"><img src="https://blog.nodebb.org/content/images/2020/11/flamegraph_before.svg" alt="Flamegraph, click to open in your browser"></a></p>
|
||||
<p>Isn't it beautiful? Alright let's dig in. Here's how to read the graph, from the flamegraph README:</p>
|
||||
<blockquote>
|
||||
<p>The <strong>y-axis</strong> shows the stack depth number. When looking at a flamegraph, the main function of your program will be closer to the bottom, and the called functions will be stacked on top, with the functions that they call stacked on top of them, etc...</p>
|
||||
<p>The <strong>x-axis</strong> spans all of the samples. It does <em>not</em> show the passing of time from left to right. The left to right ordering has no meaning.</p>
|
||||
<p>The <strong>width</strong> of each box shows the total time that that function is on the CPU or is part of the call stack. If a function's box is wider than others, that means that it consumes more CPU per execution than other functions, or that it is called more than other functions.</p>
|
||||
<p>The <strong>color</strong> of each box isn't significant, and is chosen at random.</p>
|
||||
</blockquote>
|
||||
<p>First, let's ignore anything not above <code>compiler::compile</code>, since that's really what we care about. It's clear that the majority of the program is spent in <code>compiler::parse::tokens</code>, so let's zoom in on that part by clicking on it.</p>
|
||||
<p>Now we see something like this:</p>
|
||||
<p><a href="https://nodebb.org/blog/images/optimizing-benchpress-2.png"><img src="https://nodebb.org/blog/images/optimizing-benchpress-2.png" alt="Click to open full flamegraph"></a></p>
|
||||
<p>Hmmm. Nothing sticks out to me right off the bat. All of the token parsers seem to be taking a portion of the time that matches what I'd expect.</p>
|
||||
<p>This had me stumped for a while. Then I figured out what I was missing.</p>
|
||||
<h3 id="optimization1tokenrecognition">Optimization #1: Token recognition</h3>
|
||||
<p>There are a couple of clues in the flamegraph that give it away:</p>
|
||||
<ol>
|
||||
<li><code>interp_escaped</code> and <code>interp_raw</code> share a similar amount of time. But we should be hitting <code>interp_escaped</code> way more than <code>interp_raw</code>, because escaped interpolation tokens show up far more often in the benchmark templates.</li>
|
||||
<li>There are a lot of hits on <code>new_else</code> and <code>new_end</code>, but those don't show up in the templates <em>at all</em>.</li>
|
||||
<li>We're hitting <code>from_error</code> and <code>into_result</code> a lot, which implies the parsers are erroring a lot.</li>
|
||||
</ol>
|
||||
<p>It turns out, the way token recognition works is extremely expensive. Here's the code:</p>
|
||||
<pre><code class="language-rust">let mut res = Vec::new();
|
||||
let mut index = 0; while index < input.len() { match sep.parse(input.slice(index..)) { Err(nom::Err::Error(_)) => { // do-while while { index += 1; !input.is_char_boundary(index) } {} } Err(e) => return Err(e), Ok((rest, mat)) => { // if this match was escaped, skip it if input.slice(..index).ends_with('\\') { let before_escape = input.slice(..(index - 1)); if before_escape.len() > 0 { res.push(f(before_escape)); } input = input.slice(index..); index = nom::Offset::offset(&input, &rest); continue; } if rest == input { return Err(nom::Err::Error(E::from_error_kind( rest, nom::error::ErrorKind::SeparatedList, ))); } if index > 0 { res.push(f(input.slice(..index))); } res.push(mat); input = rest; index = 0; } }
|
||||
} if index > 0 { res.push(f(input.slice(..index)));
|
||||
} Ok((input.slice(input.len()..), res))
|
||||
</code></pre>
|
||||
<p>Do you see it? For every byte in the input, we're running the full parsing suite, checking it against 10 parsers! This works, but it's quite naive. For context, Benchpress tokens come in three shapes:</p>
|
||||
<ul>
|
||||
<li>Interpolation: <code>{thing_escaped}</code>, <code>{{raw_stuff}}</code></li>
|
||||
<li>Modern Control Flow: <code>{{{ if cond }}}</code>, <code>{{{ each arr }}}</code>, <code>{{{ else }}}</code>, <code>{{{ end }}}</code></li>
|
||||
<li>Legacy Control Flow: <code><!-- IF cond --></code>, <code><!-- BEGIN arr --></code>, <code><!-- ELSE --></code>, <code><!-- END(IF) stuff --></code></li>
|
||||
</ul>
|
||||
<p>Which means that we don't need to check these at every point in the input. We only need to run the parsers when we hit an opening curly brace <code>{</code> or an opening comment-arrow <code><!--</code>. It just so happens that there's a nice rust library made exactly for the purpose of searching for multiple patterns in text: <a href="https://github.com/BurntSushi/aho-corasick">aho-corasick</a>, which also backs the Rust regex crate.</p>
|
||||
<p>Let's refactor the code to use that. I also want the refactor to handle escaping tokens here as well. aho-corasick makes this easy: we just need to define the patterns we want to look for, and tell it to use the <code>LeftmostFirst</code> mode, so it will match the escaped patterns we define first before it hits the unescaped start patterns:</p>
|
||||
<pre><code class="language-rust">static PATTERNS: &[&str] = &["\\{{{", "\\{{", "\\{", "\\<!--", "{", "<!--"]; use aho_corasick::{ AhoCorasick, AhoCorasickBuilder, MatchKind,
|
||||
};
|
||||
lazy_static::lazy_static! { static ref TOKEN_START: AhoCorasick = AhoCorasickBuilder::new() .auto_configure(PATTERNS) .match_kind(MatchKind::LeftmostFirst) .build(PATTERNS);
|
||||
}
|
||||
</code></pre>
|
||||
<p>Now we can use <code>TOKEN_START</code> in our hot loop to skip over portions of text with no tokens:</p>
|
||||
<pre><code class="language-rust">pub fn tokens(mut input: Span) -> IResult<Span, Vec<Token<'_>>> { let mut tokens = vec![]; let mut index = 0; while index < input.len() { // skip to the next `{` or `<!--` if let Some(i) = TOKEN_START.find(input.slice(index..).fragment()) { // If this is an escaped opener, skip it if i.pattern() <= 3 { let start = index + i.start(); let length = i.end() - i.start(); // Add text before the escaper character let before_escape = input.slice(..start); if before_escape.len() > 0 { tokens.push(Token::Text(before_escape)); } // Advance to after the escaper character input = input.slice((start + 1)..); // Step to after the escaped sequence index = length - 1; } else { index += i.start(); } } else { // no tokens found, break out index = input.len(); break; } match token(input.slice(index..)) { // Not a match, step to the next character Err(nom::Err::Error(_)) => { // do-while while { index += 1; !input.is_char_boundary(index) } {} } Ok((rest, tok)) => { // Token returned what it was sent, this shouldn't happen if rest == input { return Err(nom::Err::Error(nom::error::Error::from_error_kind( rest, nom::error::ErrorKind::SeparatedList, ))); } // Add test before the token if index > 0 { tokens.push(Token::Text(input.slice(..index))); } // Add token tokens.push(tok); // Advance to after the token input = rest; index = 0; } // Pass through other errors Err(e) => return Err(e), } } if index > 0 { tokens.push(Token::Text(input.slice(..index))); } Ok((input.slice(input.len()..), tokens))
|
||||
}
|
||||
</code></pre>
|
||||
<p>This looks cleaner and requires no backtracking for escaped tokens. What did it gain us in performance?</p>
|
||||
<h4 id="benchmarksround2">Benchmarks Round 2</h4>
|
||||
<pre><code class="language-sh">$ cargo +nightly bench
|
||||
running 2 tests
|
||||
test bench_compile_categories ... bench: 241,765 ns/iter (+/- 4,493)
|
||||
test bench_compile_topic ... bench: 3,969,554 ns/iter (+/- 43,614) test result: ok. 0 passed; 0 failed; 0 ignored; 2 measured; 0 filtered out $ grunt bench 2>/dev/null
|
||||
Running "benchmark" task
|
||||
compilation x 103 ops/sec ±0.38% (176 runs sampled) Done.
|
||||
</code></pre>
|
||||
<p>Nice! That's even slightly faster than benchpress-rs, so we've made a lot of progress. But there's still room for improvement.</p>
|
||||
<p>Let's see another flamegraph:</p>
|
||||
<p><a href="https://nodebb.org/blog/images/optimizing-benchpress-3.png"><img src="https://nodebb.org/blog/images/optimizing-benchpress-3.png" alt="Click to open full flamegraph"></a></p>
|
||||
<p>Look at that, tokens parsing is now a small portion of the compilation. This puts a big target on the next step: replacing the prefixer with additions to the parser.</p>
|
||||
<h3 id="optimization2prefixerforexternalkeywords">Optimization #2: Prefixer for external keywords</h3>
|
||||
<p>The "prefixer" is what I call a simple backwards-compatibility layer which runs before the parsing step. The prefixer is based on Regular Expressions, which results in it being quite slow. It does a few things:</p>
|
||||
<ul>
|
||||
<li>Detects keywords like <code>@value</code>, <code>@key</code>, <code>@index</code> outside interpolation tokens, and wraps them in curly braces: <code>{@value}</code></li>
|
||||
<li>Detects legacy loop helpers with no arguments like <code>{function.print_element}</code>, and changes them to be called with the current element as the first argument: <code>{function.print_element, @value}</code></li>
|
||||
<li>Detects legacy IF-helpers like <code><!-- IF function.works, stuff --></code> and adds the top-level context as the first argument: <code><!-- IF function.works, @root, stuff --></code>, but <em>only</em> if IF conditions</li>
|
||||
<li>Detects nested legacy <code><!-- BEGIN arr --></code> blocks and duplicates them as <code><!-- IF ./arr --><!-- BEGIN ./arr -->...<!-- ELSE --><!-- BEGIN arr -->...</code><br>
|
||||
The legacy syntax is ambiguous: templates.js would work if <code>arr</code> was a top-level value or if <code>arr</code> was a property of the current element. Because of this, we have to emit code for both cases.</li>
|
||||
</ul>
|
||||
<p>Let's move the first case into our parser instead of relying on Regex. Luckily, we just made our jobs a lot easier with that refactor of the token recognition. All we need to do is add those keywords to our aho-corasick patterns:</p>
|
||||
<pre><code class="language-rust">static PATTERNS: &[&str] = &["\\{{{", "\\{{", "\\{", "\\<!--", "{", "<!--", "@key", "@value", "@index"];
|
||||
</code></pre>
|
||||
<p>And then handle those cases in the tokens parser, parsing them as an expression and adding that to our collection of tokens:</p>
|
||||
<pre><code class="language-rust">// If this is an escaped opener, skip it
|
||||
if matches!(i.pattern(), 0..=3) { let start = index + i.start(); let length = i.end() - i.start(); // Add text before the escaper character if start > 0 { tokens.push(Token::Text(input.slice(..start))); } // Advance to after the escaper character input = input.slice((start + 1)..); // Step to after the escaped sequence index = length - 1;
|
||||
// If this is an opener, step to it
|
||||
} else if matches!(i.pattern(), 4..=5) { index += i.start();
|
||||
// If this is `@key`, `@value`, `@index`
|
||||
} else { let start = index + i.start(); let end = index + i.end(); let span = input.slice(start..end); let (_, expr) = expression(span)?; // Add text before the token if start > 0 { tokens.push(Token::Text(input.slice(..start))); } // Add token tokens.push(Token::InterpEscaped { span, expr }); // Advance to after the token input = input.slice(end..); index = 0;
|
||||
}
|
||||
</code></pre>
|
||||
<p>Let's remove the relevant parts of the prefixer and see what this bought us.</p>
|
||||
<h4 id="benchmarksround3">Benchmarks Round 3</h4>
|
||||
<pre><code class="language-sh">$ cargo +nightly bench
|
||||
running 2 tests
|
||||
test bench_compile_categories ... bench: 194,683 ns/iter (+/- 5,158)
|
||||
test bench_compile_topic ... bench: 2,109,842 ns/iter (+/- 26,943) test result: ok. 0 passed; 0 failed; 0 ignored; 2 measured; 0 filtered out $ grunt bench 2>/dev/null
|
||||
Running "benchmark" task
|
||||
compilation x 172 ops/sec ±0.26% (176 runs sampled) Done.
|
||||
</code></pre>
|
||||
<p>Another nice boost to our compilation speed. Onward!</p>
|
||||
<h3 id="optimization3legacyloophelpers">Optimization #3: Legacy loop helpers</h3>
|
||||
<p>Our second prefixer case is quite simple: when we encounter a legacy helper called with no arguments, we need to implicitly call it with <code>@value</code>. This we can implement in the expression parser.</p>
|
||||
<p>Before, we just provided an empty argument list to a legacy helper:</p>
|
||||
<pre><code class="language-rust">fn legacy_helper(input: Span) -> IResult<Span, Expression<'_>> { map( consumed(pair( preceded(tag("function."), identifier), opt(preceded( ws(tag(",")), separated_list0(ws(tag(",")), expression), )), )), |(span, (name, args))| Expression::LegacyHelper { span, name, args: args.unwrap_or_default(), // Provides an empty Vec }, )(input)
|
||||
}
|
||||
</code></pre>
|
||||
<p>But now, we want to give it <code>@value</code> in those cases, which we can achieve with <code>Option::unwrap_or_else</code>:</p>
|
||||
<pre><code class="language-rust"> args: args.unwrap_or_else(|| { // Handle legacy helpers without args being implicitly passed `@value` vec![Expression::Path(vec![PathPart::Part(Span::new_extra( "@value", input.extra, ))])] }),
|
||||
</code></pre>
|
||||
<p>Let's see the benchmarks with this removed from the prefixer.</p>
|
||||
<h4 id="benchmarksround4">Benchmarks Round 4</h4>
|
||||
<pre><code class="language-sh">$ cargo +nightly bench
|
||||
running 2 tests
|
||||
test bench_compile_categories ... bench: 187,462 ns/iter (+/- 2,519)
|
||||
test bench_compile_topic ... bench: 2,030,519 ns/iter (+/- 19,016) test result: ok. 0 passed; 0 failed; 0 ignored; 2 measured; 0 filtered out $ grunt bench 2>/dev/null
|
||||
Running "benchmark" task
|
||||
compilation x 180 ops/sec ±0.32% (181 runs sampled) Done.
|
||||
</code></pre>
|
||||
<p>Not a huge bump, likely because this doesn't show up a ton in templates, but an improvement nonetheless.</p>
|
||||
<h3 id="optimization4legacyifhelpers">Optimization #4: Legacy IF helpers</h3>
|
||||
<p>Next step in my quest to dismantle the prefixer is to handle <code><!-- IF function.foo, bar --></code>. Essentially, I need to check if the conditional is a legacy helper expression, and if so, prepend the arguments with <code>@root</code>. We can implement this in the token parser.</p>
|
||||
<p>Before, we just provided the unmodified expression:</p>
|
||||
<pre><code class="language-rust">fn legacy_if(input: Span) -> IResult<Span, Token<'_>> { map( consumed(delimited( pair(tag("<!--"), ws(tag("IF"))), ws(consumed(expression)), tag("-->"), )), |(span, (subject_raw, subject))| Token::LegacyIf { span, subject_raw, subject, // Unmodified expression }, )(input)
|
||||
}
|
||||
</code></pre>
|
||||
<p>Rust's extensive pattern-matching makes it easy to check for a <code>LegacyHelper</code> expression, and modify it accordingly:</p>
|
||||
<pre><code class="language-rust"> subject: { // Handle legacy IF helpers being passed @root as implicit first argument if let Expression::LegacyHelper { span, name, mut args } = subject { args.insert(0, Expression::Path(vec![PathPart::Part(Span::new_extra( "@root", input.extra, ))])); Expression::LegacyHelper { span, name, args } } else { subject } },
|
||||
</code></pre>
|
||||
<p>This fix step was implemented with a <a href="https://github.com/benchpressjs/benchpressjs/blob/a263019ac54e18d393e405306049f5be68e30328/benchpress_sys/src/pre_fixer.rs#L49-L57">pretty nasty Regex</a>, so I expected a pretty substantial performance improvement.</p>
|
||||
<h4 id="benchmarksround5">Benchmarks Round 5</h4>
|
||||
<pre><code class="language-sh">$ cargo +nightly bench
|
||||
running 2 tests
|
||||
test bench_compile_categories ... bench: 196,249 ns/iter (+/- 13,057)
|
||||
test bench_compile_topic ... bench: 2,028,033 ns/iter (+/- 16,110) test result: ok. 0 passed; 0 failed; 0 ignored; 2 measured; 0 filtered out $ grunt bench 2>/dev/null
|
||||
Running "benchmark" task
|
||||
compilation x 178 ops/sec ±0.37% (178 runs sampled) Done.
|
||||
</code></pre>
|
||||
<p>But really, performance was about the same, because this pattern actually never shows up in the benchmark templates. That's okay though, let's move on.</p>
|
||||
<h3 id="optimization5ambiguousinnerbegin">Optimization #5: Ambiguous inner BEGIN</h3>
|
||||
<p>With those taken care of, the only thing left is handling this:</p>
|
||||
<pre><code class="language-html"><!-- BEGIN people --> person {people.name} has the following pets: <!-- BEGIN pets --> - {pets.name} <!-- END pets -->
|
||||
<!-- END people -->
|
||||
</code></pre>
|
||||
<p>The inner <code>pets</code> loop is ambiguous. There's no way for the compiler to know that it should refer to the pets for each element of people. It could also refer to the top-level <code>pets</code> value. So we must emit code for both cases, by transforming it to this:</p>
|
||||
<pre><code class="language-html"><!-- BEGIN people --> person {people.name} has the following pets: <!-- IF ./pets --><!-- BEGIN ./pets --> - {pets.name} <!-- END pets --><!-- ELSE --><!-- BEGIN pets --> - {pets.name} <!-- END pets --><!-- ENDIF ./pets -->
|
||||
<!-- END people -->
|
||||
</code></pre>
|
||||
<p>This will be the most difficult to move into code. I won't bore you with the details, but here's a quick run-down. When this condition is detected, we interpret the tokens ahead both as if it was a relative path and as if it was an absolute path, then wrap those in a conditional. Otherwise, we emit it as we would normally.</p>
|
||||
<p>Rust's closures help with this immensely, allowing me to deduplicate the code into a closure, which I call depending on the case.</p>
|
||||
<pre><code class="language-rust">// create an iteration intruction
|
||||
Token::LegacyBegin { span, subject, subject_raw,
|
||||
} => { let normal = |input: &mut I, subject| { let mut body = vec![]; let mut alt = vec![]; let subject = resolve_expression_paths(base, subject); let base: PathBuf = if let Expression::Path(base) = &subject { let mut base = base.clone(); if let Some(last) = base.last_mut() { last.with_depth(depth) } base } else { base.to_vec() }; match tree(depth + 1, &base, input, &mut body)? { Some(Token::LegacyElse { .. }) | Some(Token::Else { .. }) => { // consume the end after the else match tree(depth, &base, input, &mut alt)? { Some(Token::LegacyEnd { .. }) | Some(Token::End { .. }) => {} _ => return Err(TreeError), } } Some(Token::LegacyEnd { .. }) | Some(Token::End { .. }) => {} _ => return Err(TreeError), } Ok(Instruction::Iter { depth, subject_raw, subject, body, alt, }) }; // Handle legacy `<!-- BEGIN stuff -->` working for top-level `stuff` and implicitly `./stuff` match &subject { Expression::Path(path) if depth > 0 && path.first().map_or(false, |s| { // Not a relative path or keyword !s.inner().starts_with(&['.', '@'] as &[char]) }) => { // Path is absolute, so create a branch for both `./subject` and `subject` let mut relative_path = vec![PathPart::Part(Span::new_extra("./", span.extra))]; relative_path.extend_from_slice(path); let relative_subject = Expression::Path(relative_path); Instruction::If { subject: resolve_expression_paths(base, relative_subject.clone()), body: vec![normal(&mut input.clone(), relative_subject)?], alt: vec![normal(input, subject)?], } } _ => normal(input, subject)?, }
|
||||
}
|
||||
</code></pre>
|
||||
<p>Yes! The prefixer is gone. Time for a final set of benchmarks.</p>
|
||||
<h4 id="finalbenchmarks">Final Benchmarks</h4>
|
||||
<pre><code class="language-sh">$ cargo +nightly bench
|
||||
running 2 tests
|
||||
test bench_compile_categories ... bench: 111,440 ns/iter (+/- 4,479)
|
||||
test bench_compile_topic ... bench: 944,981 ns/iter (+/- 6,062) test result: ok. 0 passed; 0 failed; 0 ignored; 2 measured; 0 filtered out $ grunt bench 2>/dev/null
|
||||
Running "benchmark" task
|
||||
compilation x 327 ops/sec ±0.53% (180 runs sampled) Done.
|
||||
</code></pre>
|
||||
<p>Check that out! We're 10x faster than we started, and 4.4x faster than benchpress-rs. <em>Fantastic</em>.</p>
|
||||
<p>Let's take a final look at the flamegraph:</p>
|
||||
<p><a href="https://nodebb.org/blog/images/optimizing-benchpress-4.png"><img src="https://nodebb.org/blog/images/optimizing-benchpress-4.png" alt="Click to open full flamegraph"></a></p>
|
||||
<p>Now we can see that our program spends about equal time parsing and generating output.</p>
|
||||
<h2 id="finalremarks">Final Remarks</h2>
|
||||
<p>After all of that, I was quite proud of myself. Ditching the prefixer also has a huge side-benefit: true source location information. Having that information allows me to emit some helpful Rust-inspired warnings like this:</p>
|
||||
<pre><code class="language-text">[benchpress] warning: keyword outside an interpolation token is deprecated --> tests/templates/source/loop-tokens-conditional.tpl:3:39 | 3 | <span class="label label-primary">@key</span> | ^^^^ help: wrap this in curly braces: `{@key}` | note: This will become an error in the v3.0.0
|
||||
</code></pre>
|
||||
<p>Overall, I'm very happy with <strong>nom</strong>. It made the rewrite much easier than it would have been, had I tried to custom write another parser. It made the resulting code much easier to read and more maintainable. And without its flexibility, I would never have been able to refactor out the prefixer.</p>
|
||||
</div>
|
39
blog_source/posts/the-api-continues-to-evolve.html
Normal file
|
@ -0,0 +1,39 @@
|
|||
<div class="kg-card-markdown"><p>A couple months back as part of our <a href="https://blog.nodebb.org/api-continues-to-evolve/link">Roadmap to v2</a>, I made the claim that one of the large features in that release would be the merging of the <a href="//github.com/NodeBB/nodebb-plugin-write-api">Write API plugin</a> into core. The majority of the exploratory work had been completed in a development branch reserved for v2-only changes, but the need for a consistent RESTful API became more and more important, and we simply could not wait for v2 (which hadn't and still hasn't, a release date) to drop.</p>
|
||||
<p>So <a href="https://github.com/NodeBB/NodeBB/pull/8708/commits/3dc9324ed60bc2e331b22dc70e0e253e0d3eaf62">two months ago</a>, I started pulling out this work to a separate branch based off of <code>master</code>, and set about to finishing the integration. I'm proud to say that the preliminary release of this API has been merged into core, and is available starting v1.15.0.</p>
|
||||
<p>Better yet, <a href="https://blog.nodebb.org/unveiling-of-the-read-api/">the docs have been much improved, and are now maintained similar to the Read API</a>, using the OpenAPI v3 format, and can be found here ?</p>
|
||||
<p><a href="//docs.nodebb.org/api/write">Write API Documentation</a></p>
|
||||
<h2 id="isntwriteapiamisnomer">Isn't "Write API" a misnomer?</h2>
|
||||
<p>Yes! In a sense, it is a transitionary title while the API evolves over time.</p>
|
||||
<ul>
|
||||
<li>The <strong>Read API</strong>, such as it is, contains a number of non-<code>GET</code> routes
|
||||
<ul>
|
||||
<li>These would mostly be upload-specific routes (avatars, cover photos, topic thumbnails, etc.)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>The <strong>Write API</strong>, on the other hand, <em>does</em> contain a couple of <code>GET</code> routes, and will be gaining more, over time.</li>
|
||||
</ul>
|
||||
<p>The nomenclature comes from its predecessor, <a href="//github.com/NodeBB/nodebb-plugin-write-api"><code>nodebb-plugin-write-api</code></a>. I wanted to keep the feature name similar so as to reduce confusion.</p>
|
||||
<p>Over time, we intend to introduce additional GET routes, especially for new feature construction (or rewrites of such). For example, the <em>topic thumbnail</em> functionality is currently undergoing a rewrite, and will using the Write API exclusively.</p>
|
||||
<p>All routes using the new Write API are visually separate from the Read API routes in that they all begin with <code>/api/v3</code> (e.g. <code>POST api/v3/topics</code> to create a new topic.)</p>
|
||||
<p>Eventually, the plan is to rename the <em>Write API</em> into the <em>REST API</em> (imaginative, I know.)</p>
|
||||
<h2 id="doesthismeanthedeathoftheapiprefix">Does this mean the death of the <code>/api</code> prefix?</h2>
|
||||
<p>Not at all! The existing API will continue to be maintained. <strong>We have no plans to deprecate this API</strong>. It will likely be renamed from <em>Read API</em> to <em>Page API</em> or similar. This is purely for aesthetics.</p>
|
||||
<p>Oftentimes, we would tell clients that any page you can browse to, you can see its underlying data by prepending <code>/api</code> onto that page's path. This remains true today, but we also want the reverse to be true. This means additional routes would be removed from the Read API and ported over to the Write API.</p>
|
||||
<p>NodeBB's existing frontend uses the Read API exclusively to render page templates, and to that end the Read API is functioning as intended. The Write API was originally intended to allow external services (and specific usage from within NodeBB) leaner access to forum resources.</p>
|
||||
<h2 id="whatswiththeapiv3prefix">What's with the <code>/api/v3</code> prefix?</h2>
|
||||
<p>My plan with the Write API was to iterate on version numbers whenever there were breaking changes. There was one major change, which brought the plugin's prefix to <code>api/v2</code>, and for this move, we will be bumping it <strong>one last time</strong> (more on why below) to <code>api/v3</code>. I chose to continue with <code>api/v3</code> as it would not conflict with users who wished to use the Write API plugin in parallel with a newer version of NodeBB.</p>
|
||||
<p>Keep in mind: Most users of the Write API should be able to migrate to <code>v3</code> with no significant issues.</p>
|
||||
<p><a href="https://github.com/NodeBB/NodeBB/pull/8708#issuecomment-705780711">The full list of breaking changes from <code>v2</code> to <code>v3</code> can be found here</a>.</p>
|
||||
<p>Eventually, I intend to introduce a versioning system for the API, <a href="https://stripe.com/blog/api-versioning">inspired by the Stripe API team</a>. They release new versions of their API prolifically, versioned by the date (e.g. <code>2017-05-24</code>), and requests sent in to the API with that specific version number will instruct Stripe to mutate the response to match what the response would have looked like under that version. It absolutely blows my mind how that worked, and my plan is to mimic something similar for successive releases of the API.</p>
|
||||
<h2 id="howdoesthisplaywithnodebbswebsocketimplementation">How does this play with NodeBB's websocket implementation?</h2>
|
||||
<p>The websockets/socket.io interface was never meant to be for external use. We rely on websockets for real-time events, such as new posts, notifications, online indicators, etc.</p>
|
||||
<p>I fully admit (on behalf of my peers Baris and Andrew) that we abused this system to also handle simpler requests for information, or to instruct the server to do things.</p>
|
||||
<p>These <em>call-and-response</em> type of requests were not what socket.io was designed for. That said, it handles these types of actions without breaking a sweat, but why reinvent the wheel when a battle-hardened version exists in the form of HTTP requests?</p>
|
||||
<p>Socket.io even falls back to using XHR when a websocket connection is not available, imagine that!</p>
|
||||
<p><strong>Let me be clear</strong>, we have no intention of dropping our use of socket.io. When we implement real-time events on websockets/socket.io, they work really really well, and it would be much easier to treat our use of websockets simply as a form of progressive enhancement, rather than an integral part of our software which won't work without it.</p>
|
||||
<p>There is one (rather significant) additional reason why I personally wanted to switch to using a REST API — many of our clients don't have the technical knowledge to interface via websockets. This was the original reason why the Write API was created, but if a client wanted functionality that wasn't provided by that API, they were essentially SOL because domain knowledge on how to connect to a websocket server was far and few between. HTTP is a no-brainer, and many <em>many</em> developers know how to handle and negotiate responses with external APIs.</p>
|
||||
<p>As an extension to that reason — if clients couldn't easily figure it out, <strong>neither could (or would) white-hat hackers</strong>. This meant that we had a blind spot in our websocket implementation, in that fewer eyes were looking at the code, and fewer people were trying to break in, simply due to the relative obscurity of websockets. While security by obscurity can be a part of your security plan, relying solely on security by obscurity is <strong>plain bad practice</strong>. The easier it is to interface with NodeBB, the easier it will be for clients to use, and for us to feel assured that it is implemented in a safe way.</p>
|
||||
<h2 id="theremaining10">The remaining 10%</h2>
|
||||
<p>Getting the rest of the call-and-response usages of websockets migrated to the Write API is the last 10%, and of course, the 10% is the hardest and dullest work. Once we're done this, we'll likely move forward with the name change from <strong>Write API</strong> to <strong>REST API</strong>.</p>
|
||||
<p>The average end-user of NodeBB shouldn't see a single bit of difference in their every day use. If nobody can see what I'm doing, I'd consider the migration a success ?</p>
|
||||
</div>
|
18
blog_source/posts/the-faceless-master.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
<div class="kg-card-markdown">
|
||||
|
||||
<p>This past month, my family asked me to design a website to showcase my late grandfather’s paintings. Grandpa passed away in ’93, the same year that I was born. We visited him when I was a baby and he held me. There’s a picture of it somewhere… but I never had the chance to really know him. When I was first asked to put his website together, I felt compelled to work on it primarily because I had the most technical experience of anyone in my family in web design. But not long after beginning the project, unexpectedly, it became a personally emotional journey. While working on the site, I enjoyed looking at Grandpa’s art and choosing each spot for his paintings on the site’s gallery. He was quite the painter in his time. Once featured in TIME Magazine in 1950, Grandpa won several prestigious artistic awards, with pieces showcased in New York, Pittsburgh, Chicago and Washington D.C. As I designed his site, I found the time I had with each of his paintings to be a visually moving experience. When viewing his artwork, I could look at it and exist in a moment just as I did laying in his lap 27 years ago as a baby, not really knowing him, but seeing him, if only for a brief moment.</p>
|
||||
|
||||
<p>Often times when people create forums, we begin with a basic technical reason why. Forums offer places for our users to hangout, discuss the platform, etc. ‘The users are the center of everything we do’. At times, though, the forums we create become quite a bit more than we initially anticipated. When users are viewed as people, people become friends, and in that space, a community develops and thrives.</p>
|
||||
|
||||
<p>My name is Nick Weiner and I grew up using forums such as Neopets, Gaia Online, and various phpBB-based communities. Like many of you, forums were my connection to many friends I would never have the opportunity to meet in person. Speaking at a conference in 2008, former Gaia Online CEO, Craig Sherman, said “Anonymous forums allow an extra level of getting to separate yourself from your core who you are and allow a little less consequences, and therefore, you can connect with like-minded souls in a way that’s harder to in the real world”. Sherman explained it as a similar experience to meeting someone on an airplane, opening up with them about everything in your life, and doing so because you knew then that you would likely never see them again (SGS,2008). In that core separation when our identity is separated from real consequences there are new opportunities to act on all sorts of impulses. Anonymity gives every person who uses forums some power over their identity.</p>
|
||||
|
||||
|
||||
<p>Despite never knowing my grandpa, he left behind 63 paintings, each of which conveyed a full range of emotions, passion, understanding, thoughts, and feelings. I found him as a faceless master who chose to share his identity in a profoundly beautiful way. Through his artwork, I was able to deeply connect with him 27 years after he passed away. I believe that, despite the way we have previously thought about our forums, it is time that we take a moment to reflect on what each person who uses our forums is trying to convey while using our sites as a master of their own identities. John F. Kennedy once said “So, let us not be blind to our differences -- but let us also direct attention to our common interests. Our most basic common link is that we all inhabit this small planet. We all breathe the same air. We all cherish our children's future. And we are all mortal”. It changes everything knowing that because of our anonymity, we can actively celebrate identity and understand the behaviors of those within our communities. Forums allow us to be artists and masters of our own identities. Knowing that, no matter where we are presently, each of us now have the opportunity to create stories that will reveal to our grandchildren what we were all about.</p>
|
||||
|
||||
<p>
|
||||
References:
|
||||
<a href="http://www.abeweinerpaintings.com">Abe Weiner Paintings</a>
|
||||
<a href="https://www.youtube.com/watch?v=NHoK19DRnT4">SGS2008: Casual MMOs and Immersive Worlds</a>
|
||||
Cover Photo by <a href="https://unsplash.com/photos/WukitUSJRgY">Warren Wong on Unsplash</a>
|
||||
</p>
|
||||
</div>
|
79
blog_source/posts/unveiling-of-the-read-api.html
Normal file
|
@ -0,0 +1,79 @@
|
|||
<div class="kg-card-markdown">
|
||||
|
||||
Developer empowerment has always been at the core of NodeBB:
|
||||
<ul>
|
||||
<li>Plugins receive first-class treatment, in that there are plenty of hooks enabling plugins to interact with nearly all facets of NodeBB</li>
|
||||
<li>Themes play a central role in NodeBB, allowing designers to fully realise their vision without being hamstrung by unnecessary limitations.</li>
|
||||
<li>Our open source community is thriving due in no small part to our community forum of developers helping each other</li>
|
||||
</ul>
|
||||
However, one consistent pain point has been our API, and its rather opaque discovery model.
|
||||
|
||||
More often than I care to admit, we'll receive a query: "Where are your Read API docs?", to which our response is a conciliatory "prepend <code>/api</code> to any route in order to see the underlying JSON". After all, we use the same APIs to render every single page in NodeBB, and if it's good enough for us, then it ought to be good enough for you!
|
||||
|
||||
However, this sort of solution totally sucks (pardon my French) if you don't have experience working <strong>with</strong> NodeBB. The core developers all have the full context behind most parts of the code, so we know the full capabilities of the backend, and where to look when we want to edit something. On the other hand, developers new to NodeBB don't have this benefit, and given that we're not a small project, it can definitely feel overwhelming to try to find out how everything works, or where specific logic lives in NodeBB.
|
||||
|
||||
The lack of consistent API documentation also implicitly enforces a "develop-first" approach, where you simply get started on a feature by diving into the code and figure out the plan and design later. While this approach works really well for <em>hackers</em><sup>1</sup>, it lets down the <em>planners</em>, and we want to take steps to ease development for both types of developers.
|
||||
|
||||
Another downside of our pre-existing API is that it is not entirely RESTful. The API has been <code>GET</code>-exclusive for the most part. Whatever few places we don't do <code>GET</code>s are for uploads and non-JSON related exports. Almost every action to write to NodeBB is done via websockets, which we are intending to partially deprecate<sup>2</sup>.
|
||||
|
||||
Lastly, our API is mostly displaying what we need on the page (to render properly client-side). In many cases, that dovetails nicely with developer expectations, but the nomenclature and structure need revising in order to follow API best-practices. A quick example would be, <code>/users</code> shows the user list, but individual users are under the <code>/user</code> prefix (no plural).
|
||||
<h2 id="whatdo">What do?</h2>
|
||||
The first order of business is to shine a light on the black box, our opaque Read API.
|
||||
|
||||
The more documentation we have, the easier it will be to spot opportunities for improvement. More importantly, better documentation empowers developers with better context for their planning.
|
||||
|
||||
We settled on the OpenAPI 3 specification (formerly known as Swagger), for a couple of reasons:
|
||||
<ul>
|
||||
<li>A standardised API schema allows for consistent consumption via automated tooling</li>
|
||||
<li>We can achieve some spectacular out-of-the-box styling via <a href="https://github.com/Redocly/redoc">ReDoc</a>.</li>
|
||||
</ul>
|
||||
A huge thanks to <a href="https://github.com/akhoury">Aziz Khoury </a> for getting us started on this. He singlehandedly implemented a script that allowed for automatic generation of OpenAPI docs (including request and response schemas) by examining the content that went in and out of our <a href="//community.nodebb.org">community forum</a>. Right off the bat, we had nearly complete, workable documentation! ??
|
||||
|
||||
The rest of it<sup>3</sup> was done by hand by the core developers, and <a href="https://github.com/NodeBB/NodeBB/blob/master/public/openapi/read.yaml">the document itself</a> will be a living document, that grows with the project.
|
||||
|
||||
So without further ado, introducing the <a href="https://docs.nodebb.org/api/">NodeBB Read API Documentation</a>! ??
|
||||
<h2 id="sowhat">So what?</h2>
|
||||
If you <a href="https://github.com/NodeBB/NodeBB/blob/master/public/openapi/read.yaml">look at the actual spec</a>, it's not really all that exciting. It's a bunch of YAML that looks nigh-impenetrable to the laymen<sup>4</sup>.
|
||||
|
||||
The real cool advantages come in two stages:
|
||||
<h3 id="1redoc">1. ReDoc</h3>
|
||||
We integrated ReDoc to style our documentation. We loved the design and found that it gave our API some much needed polish. <a href="https://docs.nodebb.org/api/">If you go to our Read API documentation now</a>, you'll see ReDoc as the front-end serving the behind-the-scenes YAML file.
|
||||
<h3 id="2testingsuiteintegration">2. Testing Suite Integration</h3>
|
||||
The OpenAPI document itself is tied in to our testing suite, and so every time a change is made to the public API, a corresponding change <strong>must</strong> be made to the OpenAPI document as well, otherwise the tests will fail.
|
||||
|
||||
This step is important because it keeps us accountable and ensures that the documentation itself is up-to-date with the latest code. Documentation is often one of the hardest things to keep up-to-date, second only to <strong>writing</strong> the actual documentation, so you can imagine why a tool is needed to gently (or in some cases, no-so-gently) remind developers to keep the API documentation up to date!
|
||||
<h3 id="3betterchangetracking">3. Better change tracking</h3>
|
||||
We will now also be able to see how the API changes between version releases, which begets the question of whether this aids us in our steps towards adopting <a href="https://semver.org/">semver</a>.
|
||||
<h2 id="letstakeastepback">Let's take a step back...</h2>
|
||||
In March, <a href="https://blog.nodebb.org/slice-and-dice-ndash-bringing-nodebb-back-to-basics/">I talked about taking a step back to view a project under a more objective lens</a>. Oftentimes developers (myself included!) will dive so deeply into a feature or project, and become so invested in its success, that we don't often consider whether it (or its implementation) is a good idea in the first place.
|
||||
|
||||
So let's take the time now to take a step back and view the OpenAPI spec under an objective lens:
|
||||
|
||||
I will admit that initial adoption is painful. The integration into our testing suite will introduce a failure point during our development workflow).
|
||||
|
||||
However, the benefits potentially outnumber the downsides:
|
||||
<ul>
|
||||
<li>We should now be able to catch bugs where output is changed by accident, or if property types have changed.</li>
|
||||
<li>We gain savings in developer frustration, as new developers try to integrate with NodeBB for the first time.</li>
|
||||
</ul>
|
||||
Time will tell as to whether we will discover more benefits, or run into additional downsides. We are hoping that the Read API documentation is a step in the right direction for all parties involved.
|
||||
<h2 id="whatsnext">What's next?</h2>
|
||||
Now that we have a published OpenAPI spec for our Read API, the obvious next step would be to adopt this for the Write API as well.
|
||||
|
||||
Our Write API capabilities are currently served by our <a href="https://github.com/NodeBB/nodebb-plugin-write-api">Write API plugin</a>. Currently, we're in the planning and initial development stages of merging the Write API into core. This is <a href="https://blog.nodebb.org/looking-ahead-to-nodebb-v2-x/">scheduled to land in NodeBB v2</a>.
|
||||
|
||||
The <a href="https://github.com/NodeBB/nodebb-plugin-write-api/blob/master/routes/v2/readme.md">current documentation is maintained by hand</a>, and does not offer any of the benefits of OpenAPI.
|
||||
|
||||
Additionally, while the majority of the "write-type" calls in NodeBB are handled via socket.io, we intend to replace these calls with calls to the Write API.
|
||||
|
||||
A potential stretch goal here would be strict versioning of the API with built-in backwards compatibility. We want to model this after Stripe's API, which allows you to pass in outdated payloads under a version header, and receive the response as though you were still calling that outdated API.
|
||||
|
||||
====
|
||||
|
||||
<sup>1</sup> "hacker" in this context, refers to the mindset of figuring out how something works in order to extend or modify it. In modern times, the term has evolved to mean someone gaining unauthorized access to a resource, usually with malicious intent, but I am using it in the strictly exploratory meaning.
|
||||
<sup>2</sup> <strong><em>WHAT?!</em></strong>, you say? All will be explained, in a future blog post. Don't worry, socket.io won't go away, we're just reinforcing our commitment to the Write API.
|
||||
<sup>3</sup> Pun not intended.
|
||||
<sup>4</sup> For what it's worth, it's pretty impenetrable by developers too.
|
||||
<sup>Cover Photo</sup> <a href="https://unsplash.com/photos/_-hjiem5TqI">Sincerely Media on Unsplash</a>
|
||||
|
||||
</div>
|
|
@ -0,0 +1,18 @@
|
|||
<div class="kg-card-markdown">
|
||||
<p>We were recently engaged by the SEO services company <a href="//moz.com">Moz</a> to re-vamp their Q&A forum. As is typical of many of our client engagements, not only do we do our best to migrate an existing forum to NodeBB, but we try to take any lessons we learn from the project, and apply it to all NodeBBs, going forward. In this case, Moz advised us on some new best-practices for migration work, and did an informal audit on our existing SEO implementations.</p>
|
||||
|
||||
<p>As always, the best way to build a presence and rank higher on search engines is to <strong>build quality content</strong>. Short of curated, handwritten content via a blog, the next best thing is to capture the buzz surrounding your product or organization, via a publicly indexable forum such as NodeBB. Search engines take many signals into account, beyond simple "linkbacks" and keywords – not only do we now need to ensure we publish top-quality content, we need to ensure it is both fresh and relevant as well.</p>
|
||||
|
||||
<p>Moz is by far the top reference when it comes to SEO research. In an ever-changing field, Moz manages to stay on top and deliver actionable content to webmasters worldwide. We are super excited to be hosting their Q&A forum, and wish the Moz team all the best.</p>
|
||||
|
||||
<p>Last week (on my birthday, no less ?!) they published a write-up on their migration to NodeBB, including some challenges and pitfalls. More importantly, they shared some metrics from before and after the migration, showing conclusive proof that it is possible to have your cake (a new community forum software) and eat it too (not lose traffic/market share)!</p>
|
||||
|
||||
<h6 id="checkouttheirwriteuphere"><a href="https://moz.com/blog/moz-qa-migration-case-study?utm_source=nodebb&utm_medium=blog">Check out their write-up here</a></h6>
|
||||
|
||||
<p>Thanks Moz!</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<sup>Cover Photo</sup> <a href="https://unsplash.com/@chiklad?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Ochir-Erdene Oyunmedeg</a> on <a href="https://unsplash.com/s/photos/green-grass?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>
|
||||
|
||||
</div>
|