base setup
This commit is contained in:
0
at_django_boilerplate/blogs/__init__.py
Executable file
0
at_django_boilerplate/blogs/__init__.py
Executable file
BIN
at_django_boilerplate/blogs/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
at_django_boilerplate/blogs/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
at_django_boilerplate/blogs/__pycache__/admin.cpython-312.pyc
Normal file
BIN
at_django_boilerplate/blogs/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
at_django_boilerplate/blogs/__pycache__/apps.cpython-312.pyc
Normal file
BIN
at_django_boilerplate/blogs/__pycache__/apps.cpython-312.pyc
Normal file
Binary file not shown.
BIN
at_django_boilerplate/blogs/__pycache__/models.cpython-312.pyc
Normal file
BIN
at_django_boilerplate/blogs/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
BIN
at_django_boilerplate/blogs/__pycache__/urls.cpython-312.pyc
Normal file
BIN
at_django_boilerplate/blogs/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
at_django_boilerplate/blogs/__pycache__/views.cpython-312.pyc
Normal file
BIN
at_django_boilerplate/blogs/__pycache__/views.cpython-312.pyc
Normal file
Binary file not shown.
6
at_django_boilerplate/blogs/admin.py
Executable file
6
at_django_boilerplate/blogs/admin.py
Executable file
@@ -0,0 +1,6 @@
|
||||
from django.contrib import admin
|
||||
from .models import (Blog)
|
||||
|
||||
@admin.register(Blog)
|
||||
class BlogModelAdmin(admin.ModelAdmin):
|
||||
list_display = ('title','published_on','is_draft','is_active')
|
||||
7
at_django_boilerplate/blogs/apps.py
Executable file
7
at_django_boilerplate/blogs/apps.py
Executable file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BlogsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'at_django_boilerplate.blogs'
|
||||
|
||||
45
at_django_boilerplate/blogs/generate_dummy_blogs.py
Executable file
45
at_django_boilerplate/blogs/generate_dummy_blogs.py
Executable file
@@ -0,0 +1,45 @@
|
||||
import random
|
||||
import requests
|
||||
from django.utils import timezone
|
||||
from django.core.files.base import ContentFile
|
||||
from faker import Faker
|
||||
from .models import Blog
|
||||
from accounts.models import CustomUser
|
||||
|
||||
fake = Faker()
|
||||
|
||||
def create_dummy_blogs(n=20):
|
||||
users = list(CustomUser.objects.filter(is_active=True))
|
||||
if not users:
|
||||
print("No active users found. Please create users first.")
|
||||
return
|
||||
|
||||
for i in range(n):
|
||||
title = fake.sentence(nb_words=6)
|
||||
content = fake.paragraph(nb_sentences=10)
|
||||
meta_title = title
|
||||
meta_description = fake.text(max_nb_chars=180)
|
||||
meta_keywords = ', '.join(fake.words(nb=5))
|
||||
|
||||
# Download a random image
|
||||
image_url = "https://source.unsplash.com/600x400/?realestate,home,apartment"
|
||||
response = requests.get(image_url)
|
||||
|
||||
blog = Blog(
|
||||
title=title,
|
||||
content=content,
|
||||
meta_title=meta_title,
|
||||
meta_description=meta_description,
|
||||
meta_keywords=meta_keywords,
|
||||
published_on=timezone.now(),
|
||||
published_by=random.choice(users),
|
||||
is_draft=False,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
image_name = f"blog_image_{i}.jpg"
|
||||
blog.cover_image.save(image_name, ContentFile(response.content), save=False)
|
||||
|
||||
blog.save()
|
||||
print(f"Created blog: {title}")
|
||||
34
at_django_boilerplate/blogs/migrations/0001_initial.py
Normal file
34
at_django_boilerplate/blogs/migrations/0001_initial.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 5.2.6 on 2025-12-29 05:20
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Blog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('published_on', models.DateTimeField(auto_created=True, default=django.utils.timezone.now)),
|
||||
('title', models.CharField(blank=True, max_length=80, null=True)),
|
||||
('content', models.TextField(blank=True, null=True)),
|
||||
('cover_image', models.ImageField(blank=True, null=True, upload_to='blogs')),
|
||||
('meta_title', models.CharField(blank=True, max_length=80, null=True)),
|
||||
('meta_description', models.CharField(blank=True, max_length=180, null=True)),
|
||||
('meta_keywords', models.CharField(blank=True, max_length=100, null=True)),
|
||||
('is_draft', models.BooleanField(default=True)),
|
||||
('is_active', models.BooleanField(default=False)),
|
||||
('published_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
19
at_django_boilerplate/blogs/migrations/0002_alter_blog_id.py
Normal file
19
at_django_boilerplate/blogs/migrations/0002_alter_blog_id.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.2.6 on 2026-01-05 04:33
|
||||
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('blogs', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='blog',
|
||||
name='id',
|
||||
field=models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.6 on 2026-01-05 05:39
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('blogs', '0002_alter_blog_id'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='blog',
|
||||
options={'ordering': ['-published_on'], 'verbose_name': 'Blog Post', 'verbose_name_plural': 'Blog Posts'},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='blog',
|
||||
index=models.Index(fields=['-published_on'], name='blogs_blog_publish_fdee8e_idx'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 5.2.6 on 2026-01-07 04:42
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('blogs', '0003_alter_blog_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='blog',
|
||||
options={},
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name='blog',
|
||||
name='blogs_blog_publish_fdee8e_idx',
|
||||
),
|
||||
]
|
||||
0
at_django_boilerplate/blogs/migrations/__init__.py
Executable file
0
at_django_boilerplate/blogs/migrations/__init__.py
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
17
at_django_boilerplate/blogs/models.py
Executable file
17
at_django_boilerplate/blogs/models.py
Executable file
@@ -0,0 +1,17 @@
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from at_django_boilerplate.utils.mixins import UUIDMixin
|
||||
|
||||
class Blog(UUIDMixin):
|
||||
title = models.CharField(max_length=80,null=True,blank=True)
|
||||
content = models.TextField(null=True,blank=True)
|
||||
cover_image = models.ImageField(upload_to='blogs',null=True,blank=True)
|
||||
meta_title = models.CharField(max_length=80,null=True,blank=True)
|
||||
meta_description = models.CharField(max_length=180,null=True,blank=True)
|
||||
meta_keywords = models.CharField(max_length=100,null=True,blank=True)
|
||||
published_on = models.DateTimeField(auto_created=True,default=timezone.now)
|
||||
published_by = models.ForeignKey('accounts.CustomUser',null=True,blank=True,on_delete=models.DO_NOTHING)
|
||||
is_draft = models.BooleanField(default=True)
|
||||
is_active = models.BooleanField(default=False)
|
||||
|
||||
|
||||
45
at_django_boilerplate/blogs/templates/blog_detail.html
Executable file
45
at_django_boilerplate/blogs/templates/blog_detail.html
Executable file
@@ -0,0 +1,45 @@
|
||||
{% extends "public_base.html" %}
|
||||
{% load static %}
|
||||
{% block title %}{{ meta_title }}{% endblock %}
|
||||
|
||||
{% block meta %}
|
||||
<meta name="description" content="{{ meta_description }}">
|
||||
<meta property="og:title" content="{{ meta_title }}">
|
||||
<meta property="og:description" content="{{ meta_description }}">
|
||||
<meta name="twitter:title" content="{{ meta_title }}">
|
||||
<meta name="twitter:description" content="{{ meta_description }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-4xl mx-auto px-6 py-16">
|
||||
|
||||
<!-- Back Button -->
|
||||
<div class="mb-6">
|
||||
<a href="{% url 'blogs_list' %}"
|
||||
class="inline-block px-4 py-2 bg-[#0097b2] text-white rounded-md hover:bg-[#007a94] transition-colors duration-300">
|
||||
← Back to Blogs
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<article class="bg-white shadow-lg rounded-lg overflow-hidden">
|
||||
<header class="px-8 pt-8 pb-6 border-b border-gray-200">
|
||||
<h1 class="text-4xl font-extrabold text-gray-900 tracking-tight leading-tight mb-2">
|
||||
{{ blog.title }}
|
||||
</h1>
|
||||
<time class="text-gray-500 text-sm uppercase tracking-wide" datetime="{{ blog.published_on }}">
|
||||
{{ blog.published_on|date:"F d, Y" }}
|
||||
</time>
|
||||
</header>
|
||||
|
||||
{% if blog.cover_image %}
|
||||
<figure class="w-full h-96 overflow-hidden rounded-b-lg">
|
||||
<img src="{{ blog.cover_image.url }}" alt="{{ blog.title }}" class="object-cover w-full h-full transition-transform duration-500 hover:scale-105" />
|
||||
</figure>
|
||||
{% endif %}
|
||||
|
||||
<section class="prose max-w-none px-8 py-10 text-gray-700">
|
||||
{{ blog.content|linebreaks }}
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
{% endblock %}
|
||||
233
at_django_boilerplate/blogs/templates/blogs_list.html
Executable file
233
at_django_boilerplate/blogs/templates/blogs_list.html
Executable file
@@ -0,0 +1,233 @@
|
||||
{% extends 'public_base.html' %}
|
||||
|
||||
{% block title %}{{ meta_title|default:"Our Blog - RegisterYourStartup" }}{% endblock %}
|
||||
{% block meta_description %}{{ meta_description|default:"Read the latest articles, guides, and updates on company registration, compliance, startups, and business growth from RegisterYourStartup." }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
/* Existing styles unchanged */
|
||||
.glass {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.hover-lift {
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
.hover-lift:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.gradient-text {
|
||||
background: linear-gradient(90deg, #4f46e5, #7c3aed);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.blog-card-img {
|
||||
height: 220px;
|
||||
object-fit: cover;
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
.blog-card:hover .blog-card-img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Search bar styles - pure CSS only */
|
||||
.search-form {
|
||||
max-width: 680px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.search-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: 16px 60px 16px 24px;
|
||||
font-size: 1.1rem;
|
||||
border: 2px solid #e0e7ff;
|
||||
border-radius: 9999px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: #4f46e5;
|
||||
box-shadow: 0 0 0 4px rgba(79, 70, 229, 0.15);
|
||||
}
|
||||
.search-button {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: #4f46e5;
|
||||
color: white;
|
||||
border: none;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
cursor: default;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none; /* Button is decorative only */
|
||||
}
|
||||
.search-button i {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Hide non-matching cards */
|
||||
.blog-card.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* No results message */
|
||||
.no-results {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
padding: 4rem 1rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
.no-results i {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.3;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="py-16 md:py-24 bg-[#B6C3FD] relative overflow-hidden">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="text-center max-w-4xl mx-auto">
|
||||
<h1 class="text-4xl md:text-6xl font-extrabold text-gray-900 mb-6">
|
||||
Our <span class="gradient-text">Blog</span>
|
||||
</h1>
|
||||
<p class="text-xl text-gray-700 mb-8">
|
||||
Insights, guides, and updates on company registration, startup compliance, taxation, funding, and business growth in India.
|
||||
</p>
|
||||
</div>
|
||||
<div class="absolute inset-0 pointer-events-none">
|
||||
<div class="absolute top-10 left-10 w-64 h-64 bg-purple-300 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob"></div>
|
||||
<div class="absolute top-40 right-10 w-72 h-72 bg-pink-300 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-2000"></div>
|
||||
<div class="absolute -bottom-8 left-20 w-72 h-72 bg-indigo-300 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-4000"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Client-Side Search Bar -->
|
||||
<section class="py-10 bg-gray-50">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="search-form">
|
||||
<div class="search-wrapper">
|
||||
<input
|
||||
type="text"
|
||||
id="blog-search"
|
||||
placeholder="Search articles, guides, compliance tips..."
|
||||
class="search-input"
|
||||
aria-label="Search blog posts"
|
||||
autocomplete="off"
|
||||
>
|
||||
<div class="search-button" aria-hidden="true">
|
||||
<i class="fas fa-search"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live feedback (optional) -->
|
||||
<div class="text-center mt-4 text-sm text-gray-600" id="search-feedback" style="display: none;">
|
||||
Showing results for: <strong id="search-term"></strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="blog-posts" class="py-16 bg-gray-50">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8" id="blog-grid">
|
||||
{% for blog in blogs %}
|
||||
<article class="bg-white rounded-2xl shadow-lg overflow-hidden hover-lift blog-card"
|
||||
data-title="{{ blog.title|lower }}">
|
||||
{% if blog.cover_image %}
|
||||
<a href="{% url 'blog_detail' pk=blog.pk %}">
|
||||
<img src="{{ blog.cover_image.url }}" alt="{{ blog.title }}" class="w-full blog-card-img">
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'blog_detail' pk=blog.pk %}">
|
||||
<div class="w-full h-56 bg-gradient-to-br from-indigo-200 to-purple-200 flex items-center justify-center">
|
||||
<i class="fas fa-newspaper text-5xl text-indigo-600 opacity-50"></i>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<div class="p-6">
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-3">
|
||||
<a href="{% url 'blog_detail' pk=blog.pk %}" class="hover:text-indigo-600 transition">
|
||||
{{ blog.title }}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<a href="{% url 'blog_detail' pk=blog.pk %}" class="text-indigo-600 font-medium hover:text-indigo-800 transition inline-flex items-center">
|
||||
Read More <i class="fas fa-arrow-right ml-2"></i>
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
{% empty %}
|
||||
<div class="col-span-full text-center py-20">
|
||||
<i class="fas fa-book-open text-6xl text-gray-300 mb-6"></i>
|
||||
<p class="text-2xl text-gray-600">No blog posts available yet. Stay tuned!</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- No results message (initially hidden) -->
|
||||
<div class="no-results" id="no-results" style="display: none;">
|
||||
<i class="fas fa-search text-gray-300"></i>
|
||||
<p class="text-2xl">No posts found matching your search.</p>
|
||||
<p class="text-lg mt-2">Try different keywords.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Client-side Search Script -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const searchInput = document.getElementById('blog-search');
|
||||
const blogGrid = document.getElementById('blog-grid');
|
||||
const blogCards = blogGrid.querySelectorAll('.blog-card');
|
||||
const noResults = document.getElementById('no-results');
|
||||
const feedback = document.getElementById('search-feedback');
|
||||
const searchTermSpan = document.getElementById('search-term');
|
||||
|
||||
searchInput.addEventListener('input', function () {
|
||||
const query = this.value.trim().toLowerCase();
|
||||
let visibleCount = 0;
|
||||
|
||||
blogCards.forEach(card => {
|
||||
const title = card.getAttribute('data-title');
|
||||
if (title.includes(query)) {
|
||||
card.classList.remove('hidden');
|
||||
visibleCount++;
|
||||
} else {
|
||||
card.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Toggle no-results message
|
||||
if (visibleCount === 0 && query.length > 0) {
|
||||
noResults.style.display = 'block';
|
||||
} else {
|
||||
noResults.style.display = 'none';
|
||||
}
|
||||
|
||||
// Optional live feedback
|
||||
if (query.length > 0) {
|
||||
feedback.style.display = 'block';
|
||||
searchTermSpan.textContent = query;
|
||||
} else {
|
||||
feedback.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
3
at_django_boilerplate/blogs/tests.py
Executable file
3
at_django_boilerplate/blogs/tests.py
Executable file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
8
at_django_boilerplate/blogs/urls.py
Executable file
8
at_django_boilerplate/blogs/urls.py
Executable file
@@ -0,0 +1,8 @@
|
||||
from django.urls import path
|
||||
from .import views
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
path('', views.BlogsListView.as_view(), name='blogs_list'),
|
||||
path('blogs/<uuid:pk>/', views.BlogDetailView.as_view(), name='blog_detail'),
|
||||
]
|
||||
29
at_django_boilerplate/blogs/views.py
Executable file
29
at_django_boilerplate/blogs/views.py
Executable file
@@ -0,0 +1,29 @@
|
||||
from django.views.generic import ListView,DetailView
|
||||
from at_django_boilerplate.backend_admin.models import SEOConfiguration
|
||||
# Create your views here.
|
||||
|
||||
from at_django_boilerplate.blogs.models import Blog
|
||||
class BlogsListView(ListView):
|
||||
model = Blog
|
||||
template_name = 'blogs_list.html'
|
||||
context_object_name = 'blogs'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
seo = SEOConfiguration.objects.first()
|
||||
context["meta_title"] = seo.blog_meta_title if seo else "Blog - RegisterYourStartup"
|
||||
context["meta_description"] = seo.blog_meta_description if seo else "Read articles and updates from Zestato."
|
||||
return context
|
||||
|
||||
|
||||
class BlogDetailView(DetailView):
|
||||
model = Blog
|
||||
template_name = 'blog_detail.html'
|
||||
context_object_name = 'blog'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
blog = self.get_object()
|
||||
context["meta_title"] = blog.title
|
||||
context["meta_description"] = blog.meta_description if hasattr(blog, 'meta_description') else blog.short_description[:160]
|
||||
return context
|
||||
Reference in New Issue
Block a user