diff --git a/hub/services/admin/__init__.py b/hub/services/admin/__init__.py
index 75c0d33..3c79092 100644
--- a/hub/services/admin/__init__.py
+++ b/hub/services/admin/__init__.py
@@ -1,6 +1,7 @@
# Admin module initialization
# Import all admin classes to register them with Django admin
+from .articles import *
from .base import *
from .content import *
from .leads import *
diff --git a/hub/services/admin/articles.py b/hub/services/admin/articles.py
new file mode 100644
index 0000000..e6dad8c
--- /dev/null
+++ b/hub/services/admin/articles.py
@@ -0,0 +1,90 @@
+"""
+Admin configuration for Article model
+"""
+
+from django.contrib import admin
+from django.utils.html import format_html
+from django import forms
+from django.core.exceptions import ValidationError
+
+
+from ..models import Article
+
+
+class ArticleAdminForm(forms.ModelForm):
+ """Custom form for Article admin with validation"""
+
+ class Meta:
+ model = Article
+ fields = "__all__"
+
+ def clean_title(self):
+ """Validate title length"""
+ title = self.cleaned_data.get("title")
+ if title and len(title) > 50:
+ raise ValidationError("Title must be 50 characters or less.")
+ return title
+
+ def clean_excerpt(self):
+ """Validate excerpt length"""
+ excerpt = self.cleaned_data.get("excerpt")
+ if excerpt and len(excerpt) > 200:
+ raise ValidationError("Excerpt must be 200 characters or less.")
+ return excerpt
+
+
+@admin.register(Article)
+class ArticleAdmin(admin.ModelAdmin):
+ """Admin configuration for Article model"""
+
+ form = ArticleAdminForm
+
+ list_display = (
+ "title",
+ "author",
+ "image_preview",
+ "is_published",
+ "is_featured",
+ "created_at",
+ )
+ list_filter = (
+ "is_published",
+ "is_featured",
+ "author",
+ "related_service",
+ "related_consulting_partner",
+ "related_cloud_provider",
+ "created_at",
+ )
+ search_fields = ("title", "excerpt", "content", "meta_keywords")
+ prepopulated_fields = {"slug": ("title",)}
+ readonly_fields = ("created_at", "updated_at")
+
+ def image_preview(self, obj):
+ """Display image preview in admin list view"""
+ if obj.image:
+ return format_html(
+ '
', obj.image.url
+ )
+ return "No image"
+
+ image_preview.short_description = "Image"
+
+ def related_to_display(self, obj):
+ """Display what this article is related to"""
+ return obj.related_to
+
+ related_to_display.short_description = "Related To"
+
+ def get_queryset(self, request):
+ """Optimize queries by selecting related objects"""
+ return (
+ super()
+ .get_queryset(request)
+ .select_related(
+ "author",
+ "related_service",
+ "related_consulting_partner",
+ "related_cloud_provider",
+ )
+ )
diff --git a/hub/services/migrations/0034_article.py b/hub/services/migrations/0034_article.py
new file mode 100644
index 0000000..e485625
--- /dev/null
+++ b/hub/services/migrations/0034_article.py
@@ -0,0 +1,118 @@
+# Generated by Django 5.2 on 2025-06-06 07:47
+
+import django.db.models.deletion
+import hub.services.models.base
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("services", "0033_vshnappcatprice_public_display_enabled_and_more"),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="Article",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("title", models.CharField(max_length=200)),
+ ("slug", models.SlugField(max_length=250, unique=True)),
+ (
+ "excerpt",
+ models.TextField(
+ help_text="Brief description of the article", max_length=500
+ ),
+ ),
+ ("content", models.TextField()),
+ (
+ "meta_keywords",
+ models.CharField(
+ blank=True,
+ help_text="SEO keywords separated by commas",
+ max_length=255,
+ ),
+ ),
+ (
+ "image",
+ models.ImageField(
+ help_text="Title picture for the article",
+ upload_to="article_images/",
+ validators=[hub.services.models.base.validate_image_size],
+ ),
+ ),
+ (
+ "is_published",
+ models.BooleanField(
+ default=False,
+ help_text="Only published articles are visible to users",
+ ),
+ ),
+ (
+ "is_featured",
+ models.BooleanField(
+ default=False,
+ help_text="Featured articles appear prominently in listings",
+ ),
+ ),
+ ("created_at", models.DateTimeField(auto_now_add=True)),
+ ("updated_at", models.DateTimeField(auto_now=True)),
+ (
+ "author",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="articles",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ (
+ "related_cloud_provider",
+ models.ForeignKey(
+ blank=True,
+ help_text="Link this article to a cloud provider",
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="articles",
+ to="services.cloudprovider",
+ ),
+ ),
+ (
+ "related_consulting_partner",
+ models.ForeignKey(
+ blank=True,
+ help_text="Link this article to a consulting partner",
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="articles",
+ to="services.consultingpartner",
+ ),
+ ),
+ (
+ "related_service",
+ models.ForeignKey(
+ blank=True,
+ help_text="Link this article to a specific service",
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="articles",
+ to="services.service",
+ ),
+ ),
+ ],
+ options={
+ "verbose_name": "Article",
+ "verbose_name_plural": "Articles",
+ "ordering": ["-created_at"],
+ },
+ ),
+ ]
diff --git a/hub/services/models/__init__.py b/hub/services/models/__init__.py
index f5c3107..b29a71e 100644
--- a/hub/services/models/__init__.py
+++ b/hub/services/models/__init__.py
@@ -1,3 +1,4 @@
+from .articles import *
from .base import *
from .content import *
from .leads import *
diff --git a/hub/services/models/articles.py b/hub/services/models/articles.py
new file mode 100644
index 0000000..781c54c
--- /dev/null
+++ b/hub/services/models/articles.py
@@ -0,0 +1,94 @@
+from django.db import models
+from django.urls import reverse
+from django.utils.text import slugify
+from django.contrib.auth.models import User
+from django_prose_editor.fields import ProseEditorField
+from .base import validate_image_size
+from .services import Service
+from .providers import CloudProvider, ConsultingPartner
+
+
+class Article(models.Model):
+ title = models.CharField(max_length=200)
+ slug = models.SlugField(max_length=250, unique=True)
+ excerpt = models.TextField(
+ max_length=500, help_text="Brief description of the article"
+ )
+ content = ProseEditorField()
+ meta_keywords = models.CharField(
+ max_length=255, blank=True, help_text="SEO keywords separated by commas"
+ )
+ image = models.ImageField(
+ upload_to="article_images/",
+ help_text="Title picture for the article",
+ )
+ author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="articles")
+
+ # Relations to other models
+ related_service = models.ForeignKey(
+ Service,
+ on_delete=models.SET_NULL,
+ null=True,
+ blank=True,
+ related_name="articles",
+ help_text="Link this article to a specific service",
+ )
+ related_consulting_partner = models.ForeignKey(
+ ConsultingPartner,
+ on_delete=models.SET_NULL,
+ null=True,
+ blank=True,
+ related_name="articles",
+ help_text="Link this article to a consulting partner",
+ )
+ related_cloud_provider = models.ForeignKey(
+ CloudProvider,
+ on_delete=models.SET_NULL,
+ null=True,
+ blank=True,
+ related_name="articles",
+ help_text="Link this article to a cloud provider",
+ )
+
+ # Publishing controls
+ is_published = models.BooleanField(
+ default=False, help_text="Only published articles are visible to users"
+ )
+ is_featured = models.BooleanField(
+ default=False, help_text="Featured articles appear prominently in listings"
+ )
+
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ class Meta:
+ ordering = ["-created_at"]
+ verbose_name = "Article"
+ verbose_name_plural = "Articles"
+
+ def __str__(self):
+ return self.title
+
+ def save(self, *args, **kwargs):
+ # Auto-generate slug from title if not provided
+ if not self.slug:
+ self.slug = slugify(self.title)
+ counter = 1
+ while Article.objects.filter(slug=self.slug).exists():
+ self.slug = f"{slugify(self.title)}-{counter}"
+ counter += 1
+ super().save(*args, **kwargs)
+
+ def get_absolute_url(self):
+ return reverse("services:article_detail", kwargs={"slug": self.slug})
+
+ @property
+ def related_to(self):
+ """Returns a string describing what this article is related to"""
+ if self.related_service:
+ return f"Service: {self.related_service.name}"
+ elif self.related_consulting_partner:
+ return f"Partner: {self.related_consulting_partner.name}"
+ elif self.related_cloud_provider:
+ return f"Provider: {self.related_cloud_provider.name}"
+ return "General"
diff --git a/hub/services/templates/base.html b/hub/services/templates/base.html
index b549e08..93d9364 100644
--- a/hub/services/templates/base.html
+++ b/hub/services/templates/base.html
@@ -55,6 +55,7 @@
diff --git a/hub/services/templates/services/article_detail.html b/hub/services/templates/services/article_detail.html
new file mode 100644
index 0000000..051967c
--- /dev/null
+++ b/hub/services/templates/services/article_detail.html
@@ -0,0 +1,178 @@
+{% extends 'base.html' %}
+{% load static %}
+{% load contact_tags %}
+
+{% block title %}{{ article.title }}{% endblock %}
+{% block meta_description %}{{ article.excerpt }}{% endblock %}
+{% block meta_keywords %}{{ article.meta_keywords }}{% endblock %}
+
+{% block content %}
+
+
+{% if article.image %}
+
+
+
+

+
+
+
+{% endif %}
+
+
+
+
+
+
+
+ {{ article.content|safe }}
+
+
+
+
+ {% if article.related_service or article.related_consulting_partner or article.related_cloud_provider %}
+
+
Related Links
+
+ {% if article.related_service %}
+
+
+
+
Service
+ {% if article.related_service.logo %}
+
+

+
+ {% endif %}
+
{{ article.related_service.name }}
+
View Service
+
+
+
+ {% endif %}
+ {% if article.related_consulting_partner %}
+
+
+
+
Partner
+ {% if article.related_consulting_partner.logo %}
+
+

+
+ {% endif %}
+
{{ article.related_consulting_partner.name }}
+
View Partner
+
+
+
+ {% endif %}
+ {% if article.related_cloud_provider %}
+
+
+
+
Provider
+ {% if article.related_cloud_provider.logo %}
+
+

+
+ {% endif %}
+
{{ article.related_cloud_provider.name }}
+
View Provider
+
+
+
+ {% endif %}
+
+
+ {% endif %}
+
+
+ {% if related_articles %}
+
+
Related Articles
+
+ {% for related_article in related_articles %}
+
+
+ {% if related_article.image %}
+

+ {% endif %}
+
+
{{ related_article.title }}
+
{{ related_article.excerpt|truncatewords:15 }}
+
{{ related_article.created_at|date:"M d, Y" }}
+
+
+
+ {% endfor %}
+
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/hub/services/templates/services/article_list.html b/hub/services/templates/services/article_list.html
new file mode 100644
index 0000000..edb0989
--- /dev/null
+++ b/hub/services/templates/services/article_list.html
@@ -0,0 +1,217 @@
+{% extends 'base.html' %}
+{% load static %}
+{% load contact_tags %}
+
+{% block title %}Articles{% endblock %}
+{% block meta_description %}Explore all articles on Servala, covering cloud services, consulting partners, and cloud provider insights.{% endblock %}
+
+{% block content %}
+
+
+
+
+
1024 ? open : true" class="d-lg-flex">
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% for article in articles %}
+
+
+ {% if article.image or article.is_featured %}
+
+ {% if article.image %}
+
+

+
+ {% endif %}
+ {% if article.is_featured %}
+
+ Featured
+
+ {% endif %}
+
+ {% endif %}
+
+
+
+
{{ article.excerpt|truncatewords:20 }}
+
+
+
+
+ {% empty %}
+
+
+ No articles found matching your criteria.
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/hub/services/templates/services/offering_detail.html b/hub/services/templates/services/offering_detail.html
index 134dc2f..f44dddc 100644
--- a/hub/services/templates/services/offering_detail.html
+++ b/hub/services/templates/services/offering_detail.html
@@ -94,6 +94,50 @@
{% endif %}
+
+
+ {% if service_articles %}
+
+
Articles about {{ offering.service.name }}
+
+
+ {% endif %}
+
+
+ {% if provider_articles %}
+
+
Articles about {{ offering.cloud_provider.name }}
+
+
+ {% endif %}
@@ -402,4 +446,6 @@
+
+
{% endblock %}
\ No newline at end of file
diff --git a/hub/services/templates/services/partner_detail.html b/hub/services/templates/services/partner_detail.html
index 866302f..7e90b82 100644
--- a/hub/services/templates/services/partner_detail.html
+++ b/hub/services/templates/services/partner_detail.html
@@ -119,6 +119,28 @@
{% endif %}
+
+
+ {% if related_articles %}
+
+ {% endif %}
diff --git a/hub/services/templates/services/provider_detail.html b/hub/services/templates/services/provider_detail.html
index ade6e12..151c986 100644
--- a/hub/services/templates/services/provider_detail.html
+++ b/hub/services/templates/services/provider_detail.html
@@ -119,6 +119,28 @@
{% endif %}
+
+
+ {% if related_articles %}
+
+ {% endif %}
diff --git a/hub/services/templates/services/provider_list.html b/hub/services/templates/services/provider_list.html
index b343f80..5484b7e 100644
--- a/hub/services/templates/services/provider_list.html
+++ b/hub/services/templates/services/provider_list.html
@@ -98,10 +98,12 @@
diff --git a/hub/services/templates/services/service_detail.html b/hub/services/templates/services/service_detail.html
index ca73351..5054d61 100644
--- a/hub/services/templates/services/service_detail.html
+++ b/hub/services/templates/services/service_detail.html
@@ -93,6 +93,28 @@
{% endif %}
+
+
+ {% if related_articles %}
+
+ {% endif %}
diff --git a/hub/services/templatetags/social_meta_tags.py b/hub/services/templatetags/social_meta_tags.py
index d8eb107..03fe0cb 100644
--- a/hub/services/templatetags/social_meta_tags.py
+++ b/hub/services/templatetags/social_meta_tags.py
@@ -59,18 +59,68 @@ def social_meta_tags(context):
else description
)
+ elif view_name == "services:article_detail" and "article" in context:
+ article = context["article"]
+ title = f"Servala - {article.title}"
+ description = article.excerpt
+ # Use article image if available, otherwise default
+ if article.image:
+ image_url = request.build_absolute_uri(article.image.url)
+
+ # Determine og:type based on view
+ og_type = "website" # default
+ if view_name == "services:article_detail" and "article" in context:
+ og_type = "article"
+
# Generate the HTML for meta tags
tags = f"""
-
+
-
-
-
-
"""
+ # Add article-specific meta tags if this is an article detail page
+ if view_name == "services:article_detail" and "article" in context:
+ article = context["article"]
+
+ # Add article-specific Open Graph tags
+ article_tags = f"""
+
+
+
+ """
+
+ # Add article section if related to service, partner, or provider
+ if article.related_service:
+ article_tags += (
+ f'\n '
+ )
+ elif article.related_consulting_partner:
+ article_tags += (
+ f'\n '
+ )
+ elif article.related_cloud_provider:
+ article_tags += (
+ f'\n '
+ )
+ else:
+ article_tags += f'\n '
+
+ # Add meta keywords as article tags if available
+ if article.meta_keywords:
+ keywords = [
+ keyword.strip()
+ for keyword in article.meta_keywords.split(",")
+ if keyword.strip()
+ ]
+ for keyword in keywords:
+ article_tags += (
+ f'\n '
+ )
+
+ tags += article_tags
+
return mark_safe(tags)
diff --git a/hub/services/urls.py b/hub/services/urls.py
index 552ead5..2ead4b0 100644
--- a/hub/services/urls.py
+++ b/hub/services/urls.py
@@ -18,6 +18,8 @@ urlpatterns = [
),
path("provider//", views.provider_detail, name="provider_detail"),
path("partner//", views.partner_detail, name="partner_detail"),
+ path("articles/", views.article_list, name="article_list"),
+ path("article//", views.article_detail, name="article_detail"),
path("contact/", views.leads.contact, name="contact"),
path("contact/thank-you/", views.thank_you, name="thank_you"),
path("contact-form/", views.contact_form, name="contact_form"),
diff --git a/hub/services/views/__init__.py b/hub/services/views/__init__.py
index 0af9c30..56d2142 100644
--- a/hub/services/views/__init__.py
+++ b/hub/services/views/__init__.py
@@ -1,3 +1,4 @@
+from .articles import *
from .leads import *
from .offerings import *
from .partners import *
diff --git a/hub/services/views/articles.py b/hub/services/views/articles.py
new file mode 100644
index 0000000..6589d6f
--- /dev/null
+++ b/hub/services/views/articles.py
@@ -0,0 +1,176 @@
+from django.shortcuts import render, get_object_or_404
+from django.db.models import Q
+from hub.services.models import (
+ Article,
+ Service,
+ ConsultingPartner,
+ CloudProvider,
+)
+
+
+def article_list(request):
+ """View for listing articles with filtering capabilities"""
+ # Get basic filter parameters
+ search_query = request.GET.get("search", "")
+ service_id = request.GET.get("service", "")
+ consulting_partner_id = request.GET.get("consulting_partner", "")
+ cloud_provider_id = request.GET.get("cloud_provider", "")
+
+ # Start with all published articles
+ all_articles = Article.objects.filter(is_published=True)
+ articles = all_articles
+
+ # Apply filters based on request parameters
+ if search_query:
+ articles = articles.filter(
+ Q(title__icontains=search_query)
+ | Q(excerpt__icontains=search_query)
+ | Q(content__icontains=search_query)
+ | Q(meta_keywords__icontains=search_query)
+ )
+
+ if service_id:
+ articles = articles.filter(related_service__id=service_id)
+
+ if consulting_partner_id:
+ articles = articles.filter(related_consulting_partner__id=consulting_partner_id)
+
+ if cloud_provider_id:
+ articles = articles.filter(related_cloud_provider__id=cloud_provider_id)
+
+ # Order articles: featured first, then by creation date (newest first)
+ articles = articles.order_by(
+ "-is_featured", # Featured first (True before False)
+ "-created_at", # Newest first
+ )
+
+ # Create base querysets for each filter type that apply all OTHER current filters
+ # This way, each filter shows options that would return results if selected
+
+ # For service filter options, apply all other filters except service
+ service_filter_base = all_articles
+ if search_query:
+ service_filter_base = service_filter_base.filter(
+ Q(title__icontains=search_query)
+ | Q(excerpt__icontains=search_query)
+ | Q(content__icontains=search_query)
+ | Q(meta_keywords__icontains=search_query)
+ )
+ if consulting_partner_id:
+ service_filter_base = service_filter_base.filter(
+ related_consulting_partner__id=consulting_partner_id
+ )
+ if cloud_provider_id:
+ service_filter_base = service_filter_base.filter(
+ related_cloud_provider__id=cloud_provider_id
+ )
+
+ # For consulting partner filter options, apply all other filters except consulting_partner
+ cp_filter_base = all_articles
+ if search_query:
+ cp_filter_base = cp_filter_base.filter(
+ Q(title__icontains=search_query)
+ | Q(excerpt__icontains=search_query)
+ | Q(content__icontains=search_query)
+ | Q(meta_keywords__icontains=search_query)
+ )
+ if service_id:
+ cp_filter_base = cp_filter_base.filter(related_service__id=service_id)
+ if cloud_provider_id:
+ cp_filter_base = cp_filter_base.filter(
+ related_cloud_provider__id=cloud_provider_id
+ )
+
+ # For cloud provider filter options, apply all other filters except cloud_provider
+ cloud_filter_base = all_articles
+ if search_query:
+ cloud_filter_base = cloud_filter_base.filter(
+ Q(title__icontains=search_query)
+ | Q(excerpt__icontains=search_query)
+ | Q(content__icontains=search_query)
+ | Q(meta_keywords__icontains=search_query)
+ )
+ if service_id:
+ cloud_filter_base = cloud_filter_base.filter(related_service__id=service_id)
+ if consulting_partner_id:
+ cloud_filter_base = cloud_filter_base.filter(
+ related_consulting_partner__id=consulting_partner_id
+ )
+
+ # Get available services, consulting partners and cloud providers that would return results if selected
+ available_services = Service.objects.filter(
+ disable_listing=False,
+ id__in=service_filter_base.values_list(
+ "related_service__id", flat=True
+ ).distinct(),
+ ).distinct()
+
+ available_consulting_partners = ConsultingPartner.objects.filter(
+ disable_listing=False,
+ id__in=cp_filter_base.values_list(
+ "related_consulting_partner__id", flat=True
+ ).distinct(),
+ ).distinct()
+
+ available_cloud_providers = CloudProvider.objects.filter(
+ disable_listing=False,
+ id__in=cloud_filter_base.values_list(
+ "related_cloud_provider__id", flat=True
+ ).distinct(),
+ ).distinct()
+
+ context = {
+ "articles": articles,
+ "available_services": available_services,
+ "available_consulting_partners": available_consulting_partners,
+ "available_cloud_providers": available_cloud_providers,
+ "search_query": search_query,
+ }
+
+ return render(request, "services/article_list.html", context)
+
+
+def article_detail(request, slug):
+ """View for displaying article details"""
+ article = get_object_or_404(
+ Article.objects.select_related(
+ "author",
+ "related_service",
+ "related_consulting_partner",
+ "related_cloud_provider"
+ ).filter(is_published=True),
+ slug=slug,
+ )
+
+ # Get related articles (same service, partner, or provider)
+ related_articles = Article.objects.filter(
+ is_published=True
+ ).exclude(id=article.id)
+
+ if article.related_service:
+ related_articles = related_articles.filter(
+ related_service=article.related_service
+ )
+ elif article.related_consulting_partner:
+ related_articles = related_articles.filter(
+ related_consulting_partner=article.related_consulting_partner
+ )
+ elif article.related_cloud_provider:
+ related_articles = related_articles.filter(
+ related_cloud_provider=article.related_cloud_provider
+ )
+ else:
+ # If no specific relation, get other general articles
+ related_articles = related_articles.filter(
+ related_service__isnull=True,
+ related_consulting_partner__isnull=True,
+ related_cloud_provider__isnull=True
+ )
+
+ related_articles = related_articles.order_by("-created_at")[:3]
+
+ context = {
+ "article": article,
+ "related_articles": related_articles,
+ }
+ return render(request, "services/article_detail.html", context)
\ No newline at end of file
diff --git a/hub/services/views/offerings.py b/hub/services/views/offerings.py
index 4730b4a..2c876a6 100644
--- a/hub/services/views/offerings.py
+++ b/hub/services/views/offerings.py
@@ -129,10 +129,20 @@ def offering_detail(request, provider_slug, service_slug):
except VSHNAppCatPrice.DoesNotExist:
pass
+ # Get related articles for both cloud provider and service
+ provider_articles = offering.cloud_provider.articles.filter(
+ is_published=True
+ ).order_by("-created_at")[:3]
+ service_articles = offering.service.articles.filter(is_published=True).order_by(
+ "-created_at"
+ )[:3]
+
context = {
"offering": offering,
"pricing_data_by_group_and_service_level": pricing_data_by_group_and_service_level,
"price_calculator_enabled": price_calculator_enabled,
+ "provider_articles": provider_articles,
+ "service_articles": service_articles,
}
return render(request, "services/offering_detail.html", context)
diff --git a/hub/services/views/partners.py b/hub/services/views/partners.py
index 4645c69..ac3668e 100644
--- a/hub/services/views/partners.py
+++ b/hub/services/views/partners.py
@@ -84,8 +84,14 @@ def partner_detail(request, slug):
slug=slug,
)
+ # Get related articles for this partner
+ related_articles = partner.articles.filter(is_published=True).order_by(
+ "-created_at"
+ )[:3]
+
context = {
"partner": partner,
"services": partner.services.all(),
+ "related_articles": related_articles,
}
return render(request, "services/partner_detail.html", context)
diff --git a/hub/services/views/providers.py b/hub/services/views/providers.py
index 3e8b965..376d8df 100644
--- a/hub/services/views/providers.py
+++ b/hub/services/views/providers.py
@@ -64,9 +64,15 @@ def provider_detail(request, slug):
)
)
+ # Get related articles for this cloud provider
+ related_articles = provider.articles.filter(is_published=True).order_by(
+ "-created_at"
+ )[:3]
+
context = {
"provider": provider,
"services": services,
"ordered_offerings": ordered_offerings,
+ "related_articles": related_articles,
}
return render(request, "services/provider_detail.html", context)
diff --git a/hub/services/views/services.py b/hub/services/views/services.py
index fc6d275..b9dd1da 100644
--- a/hub/services/views/services.py
+++ b/hub/services/views/services.py
@@ -153,7 +153,13 @@ def service_detail(request, slug):
slug=slug,
)
+ # Get related articles for this service
+ related_articles = service.articles.filter(is_published=True).order_by(
+ "-created_at"
+ )[:3]
+
context = {
"service": service,
+ "related_articles": related_articles,
}
return render(request, "services/service_detail.html", context)
diff --git a/hub/settings.py b/hub/settings.py
index 7ffec5a..68452ad 100644
--- a/hub/settings.py
+++ b/hub/settings.py
@@ -244,6 +244,8 @@ JAZZMIN_SETTINGS = {
"url": "https://servala.com",
"new_window": True,
},
+ {"name": "Articles", "url": "/admin/services/article/"},
+ {"name": "FAQs", "url": "/admin/services/websitefaq/"},
],
"show_sidebar": True,
"navigation_expanded": True,