perf: consolidate article DB queries into single cached lookup (#38)

- Add loadArticle() with static per-request cache for article data
- Refactor getArticleDate(), getArticleAuthor() to use cached article
- Refactor findImage() for com_content to use cached article
- Pass cached article to JsonLdBuilder::buildArticle() to skip its query
- Reduces article page DB queries from 5 to 1 for OG tag generation
This commit is contained in:
Jonathan Miller
2026-06-21 11:09:52 -05:00
parent 7a7041c7f3
commit ca06c86328
2 changed files with 65 additions and 47 deletions
@@ -220,7 +220,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
if ($option === 'com_mokoshop' && $view === 'product' && $id > 0) {
$schema = JsonLdBuilder::buildProduct($id, $title, $description, $imageUrl);
} elseif ($option === 'com_content' && $view === 'article' && $id > 0) {
$schema = JsonLdBuilder::buildArticle($id, $title, $description, $imageUrl);
$schema = JsonLdBuilder::buildArticle($id, $title, $description, $imageUrl, $this->loadArticle($id));
} else {
$schema = JsonLdBuilder::buildWebPage($title, $description);
}
@@ -448,17 +448,10 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
// For Joomla articles, look at the intro/full image fields
if ($option === 'com_content' && $id > 0) {
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('images'))
->from($db->quoteName('#__content'))
->where($db->quoteName('id') . ' = ' . (int) $id);
$article = $this->loadArticle($id);
$db->setQuery($query);
$images = $db->loadResult();
if ($images) {
$imagesData = json_decode($images, true);
if ($article && !empty($article->images)) {
$imagesData = json_decode($article->images, true);
if (!empty($imagesData['image_fulltext'])) {
return $imagesData['image_fulltext'];
@@ -514,6 +507,38 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
return rtrim(Uri::root(), '/') . '/' . ltrim($image, '/');
}
/**
* Load and cache a full article record with author for the current request.
*
* @param int $id Article ID
*
* @return object|null
*/
private function loadArticle(int $id): ?object
{
static $cache = [];
if (isset($cache[$id])) {
return $cache[$id];
}
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName([
'a.title', 'a.introtext', 'a.fulltext', 'a.images',
'a.created', 'a.modified', 'a.publish_up', 'a.metadesc',
]))
->select($db->quoteName('u.name', 'author_name'))
->from($db->quoteName('#__content', 'a'))
->join('LEFT', $db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('u.id') . ' = ' . $db->quoteName('a.created_by'))
->where($db->quoteName('a.id') . ' = ' . $id);
$db->setQuery($query);
$cache[$id] = $db->loadObject();
return $cache[$id];
}
/**
* Get a date field from an article.
*
@@ -524,16 +549,9 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
*/
private function getArticleDate(int $id, string $field): string
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName($field))
->from($db->quoteName('#__content'))
->where($db->quoteName('id') . ' = ' . $id);
$article = $this->loadArticle($id);
$db->setQuery($query);
$date = $db->loadResult();
return $date ?: '';
return $article->$field ?? '';
}
/**
@@ -545,16 +563,9 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
*/
private function getArticleAuthor(int $id): string
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('u.name'))
->from($db->quoteName('#__content', 'a'))
->join('LEFT', $db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('u.id') . ' = ' . $db->quoteName('a.created_by'))
->where($db->quoteName('a.id') . ' = ' . $id);
$article = $this->loadArticle($id);
$db->setQuery($query);
return $db->loadResult() ?: '';
return $article->author_name ?? '';
}
/**
@@ -20,31 +20,36 @@ class JsonLdBuilder
/**
* Build Article schema for a com_content article.
*
* @param int $articleId Article ID
* @param string $title Page title
* @param string $description Page description
* @param string $image Image URL (absolute)
* @param int $articleId Article ID
* @param string $title Page title
* @param string $description Page description
* @param string $image Image URL (absolute)
* @param object|null $cachedArticle Pre-loaded article data (avoids duplicate query)
*
* @return array|null
*/
public static function buildArticle(int $articleId, string $title, string $description, string $image): ?array
public static function buildArticle(int $articleId, string $title, string $description, string $image, ?object $cachedArticle = null): ?array
{
if ($articleId <= 0) {
return null;
}
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName([
'a.created', 'a.modified', 'a.publish_up',
'u.name',
]))
->from($db->quoteName('#__content', 'a'))
->join('LEFT', $db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('u.id') . ' = ' . $db->quoteName('a.created_by'))
->where($db->quoteName('a.id') . ' = ' . $articleId);
$article = $cachedArticle;
$db->setQuery($query);
$article = $db->loadObject();
if (!$article) {
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName([
'a.created', 'a.modified', 'a.publish_up',
]))
->select($db->quoteName('u.name', 'author_name'))
->from($db->quoteName('#__content', 'a'))
->join('LEFT', $db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('u.id') . ' = ' . $db->quoteName('a.created_by'))
->where($db->quoteName('a.id') . ' = ' . $articleId);
$db->setQuery($query);
$article = $db->loadObject();
}
if (!$article) {
return null;
@@ -60,10 +65,12 @@ class JsonLdBuilder
'dateModified' => $article->modified ?: $article->created,
];
if (!empty($article->name)) {
$authorName = $article->author_name ?? '';
if (!empty($authorName)) {
$schema['author'] = [
'@type' => 'Person',
'name' => $article->name,
'name' => $authorName,
];
}