├── .gitignore ├── MANIFEST.in ├── README.md ├── README.txt ├── requirements.pip ├── setup.py ├── static_sitemaps ├── __init__.py ├── conf.py ├── generator.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── refresh_sitemap.py ├── tasks.py ├── templates │ └── static_sitemaps │ │ └── sitemap_index.xml ├── urls.py ├── util.py └── views.py └── testapp ├── manage.py ├── sitemaps └── .gitignore └── testapp ├── __init__.py ├── settings.py ├── sitemaps.py ├── urls.py └── wsgi.py /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .pydevproject 3 | *.pyc 4 | .idea 5 | *egg* 6 | .DS_Store 7 | dist 8 | django_static_sitemaps.egg-info 9 | .vscode 10 | __pycache__ 11 | testapp/db.sqlite3 12 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include MANIFEST.in 3 | recursive-include static_sitemaps * 4 | global-exclude __pycache__ 5 | global-exclude *.pyc 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | django-static-sitemaps 2 | ======================== 3 | 4 | There are times when your site is too big to serve the ``sitemap.xml`` by your Django application. This little app is meant to help you with such cases. Instead of serving the sitemap.xml from Django, it features a **management command**/**celery task** that generates the ``sitemap.xml`` to the separate files. 5 | 6 | Feature highlights: 7 | 8 | * **NEW:** Compatible with Django 2.0+ 9 | * **NEW:** Python 3 compatible 10 | * Generate sitemap files to your STATIC/MEDIA/(own choice) directory 11 | * Split sitemap files when limit for size is reached 12 | * gzip the sitemap files when required 13 | * Set different domain for sitemap file 14 | 15 | Python 2 users 16 | ------ 17 | 18 | Python 2 users have should use `4.4.0`. Newer versions will by Python 3 compatible only. Sorry about that. 19 | 20 | Requirements 21 | ------------ 22 | 23 | The only requirement is Django 1.8+. App should work with older Django versions with some settings 24 | tweaks. 25 | 26 | 27 | Usage 28 | ------ 29 | 30 | Install via standard Python way:: 31 | 32 | pip install django-static-sitemaps 33 | 34 | Add `static_sitemaps` to you ``INSTALLED_APPS`` and make sure 35 | `django.contrib.sitemaps` is present too: 36 | 37 | ```python 38 | INSTALLED_APPS = ( 39 | ... 40 | 'django.contrib.sites', 41 | 'django.contrib.sitemaps', 42 | ... 43 | 'static_sitemaps', 44 | ... 45 | ) 46 | 47 | SITE_ID = 1 48 | ``` 49 | 50 | Remember to run `python manage.py makemigrations` and `python manage.py migrate`. 51 | 52 | Set ``STATICSITEMAPS_ROOT_SITEMAP`` variable in your ``settings.py`` to point 53 | to dictionary holding the sitemaps configuration (as seen in Django docs):: 54 | 55 | STATICSITEMAPS_ROOT_SITEMAP = 'myproject.sitemaps.sitemaps' 56 | 57 | Also, make sure you have `STATICSITEMAPS_ROOT_DIR` or at least `STATIC_ROOT` configured. 58 | Sitemap files will be placed there. 59 | 60 | Include ``static_sitemaps.urls`` to your ``urls.py`` to serve the root 61 | ``sitemap.xml`` if you want to serve index file through Django (might be 62 | usefull sometimes when it's hard for you to serve it by webserver itself): 63 | 64 | urlpatterns = [ 65 | path('', include('static_sitemaps.urls')), 66 | ] 67 | 68 | Setup your cron to run:: 69 | 70 | django-admin.py refresh_sitemap 71 | 72 | periodically. Usually, it's enough to set it to run once by 30 minutes or so. 73 | 74 | For Windows users you can alternatively use the following command: 75 | 76 | python manage.py runserver 77 | 78 | Done. 79 | 80 | Alternatively, you can run this using a Celery task runner. For details, look below. 81 | 82 | **Note:** Your sitemap files will be served from ``STATIC_URL`` by default. If your 83 | ``STATIC_URL`` is a relative one (e.g. ``/static/``), the result will be 84 | prepended the domain to respect the current ``Site`` object. If your 85 | ``STATIC_URL`` is absolute (generally doesn't start with a '/'), sitemaps 86 | URL will respect it completely. If you need more detailed control, see 87 | ``STATICSITEMAPS_URL`` setting. 88 | 89 | **Note about sitemap index lastmod:** In the static_sitemaps app the sitemaps 90 | index works slightly different than the Django's default behaviour. Just like 91 | Django it also gathers all urls from the generated sitemaps but it also 92 | includes a new XML tag ``lastmod``. The date/time set in this tag comes 93 | from the first element of the generated file, so reverse sorting your query 94 | by your date field will keep this information accurate. This is important to 95 | inform the crawler how fresh is the information inside each sitemap inside the 96 | sitemap_index.xml. 97 | 98 | Running as celery task 99 | ---------------------- 100 | 101 | If you run celery as your task runner, you should be ready to go out of the box. django-static-sitemaps includes the ``GenerateSitemap`` task which will be automatically run each ``STATICSITEMAPS_REFRESH_AFTER`` minutes (defaults to 60 ~ 1 hour). You can optionally bypass it by setting it to ``None``. 102 | 103 | Advanced settings 104 | ------------------ 105 | 106 | ``STATICSITEMAPS_ROOT_DIR`` 107 | Filesystem path to generate the sitemaps files to. Defaults to ``STATIC_ROOT`` directory. 108 | 109 | ``STATICSITEMAPS_USE_GZIP`` 110 | Defaults to ``True``. If ``True``, gzip compression will be used when generating the sitemaps files (which is very possible by sitemaps specification). 111 | 112 | ``STATICSITEMAPS_GZIP_METHOD`` 113 | Gzip method to use. Must be in ['python', 'system', ]. 114 | 115 | ``STATICSITEMAPS_SYSTEM_GZIP_PATH`` 116 | Path to the gzip binary if use STATICSITEMAPS_GZIP_METHOD == 'system'. 117 | 118 | ``STATICSITEMAPS_FILENAME_TEMPLATE`` 119 | Template for sitemap parts. Defaults to ``sitemap-%(section)s-%(page)s.xml``. 120 | 121 | ``STATICSITEMAPS_INDEX_TEMPLATE`` 122 | Template path for sitemap index. Defaults to ``static_sitemaps/sitemap_index.xml``. 123 | 124 | ``STATICSITEMAPS_URL`` 125 | Set this to the URL from which you want to serve the sitemaps. Can be an URL with and without domain, e.g. http://example.com/media/sitemaps/ or /media/sitemaps/. 126 | If no domain is given, the domain of the current Django site is used. Default is STATIC_URL. 127 | 128 | ``STATICSITEMAPS_LANGUAGE`` 129 | Language code to use when generating the sitemaps. Defaults to ``LANGUAGE_CODE`` setting. 130 | 131 | ``STATICSITEMAPS_REFRESH_AFTER`` 132 | How often (in minutes) should the celery task be run. Defaults to 60 minutes. 133 | 134 | ``STATICSITEMAPS_MOCK_SITE`` 135 | True|False setting if you want to mock the Django sites framework. Useful if you want to use package without enabling django.contrib.sites. Defaults to False. 136 | 137 | ``STATICSITEMAPS_MOCK_SITE_NAME`` 138 | URL of the site your mocking. This is what will show up in your sitemap as the URL. For example: 'www.yoursite.com'. Defaults to None. 139 | 140 | ``STATICSITEMAPS_MOCK_SITE_PROTOCOL`` 141 | Protocol to use when mocking above site name. Defaults to 'http'. 142 | 143 | ``STATICSITEMAPS_STORAGE`` 144 | Storage class to use. Defaults to ``django.core.files.storage.FileSystemStorage``. 145 | 146 | 147 | Using a custom template 148 | ----------------------- 149 | 150 | If you need to use a template different from the Django's default (for example 151 | to generate a Google News sitemap) you can extend the you Sitemap class and 152 | setting a ``sitemap_template`` attribute. For example: 153 | 154 | from django.contrib.sitemaps import GenericSitemap 155 | 156 | class GoogleNewsSitemap(GenericSitemap): 157 | sitemap_template = 'sitemap_googlenews.xml' 158 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /requirements.pip: -------------------------------------------------------------------------------- 1 | django>=1.8 2 | six>=1.11.0 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import static_sitemaps 3 | 4 | setup( 5 | name='django-static-sitemaps', 6 | version=static_sitemaps.__versionstr__, 7 | description='Tool for generating sitemaps as static files', 8 | long_description='\n'.join(( 9 | '', 10 | )), 11 | author='Filip Varecha', 12 | author_email='filip@varecha.work', 13 | license='BSD', 14 | url='http://github.com/xaralis/django-static-sitemaps', 15 | 16 | packages=( 17 | 'static_sitemaps', 18 | 'static_sitemaps.management', 19 | 'static_sitemaps.management.commands'), 20 | 21 | include_package_data=True, 22 | 23 | classifiers=[ 24 | "Development Status :: 4 - Beta", 25 | "Intended Audience :: Developers", 26 | "License :: OSI Approved :: BSD License", 27 | "Framework :: Django", 28 | "Programming Language :: Python :: 2.5", 29 | "Programming Language :: Python :: 2.6", 30 | "Programming Language :: Python :: 2.7", 31 | "Programming Language :: Python :: 3.4", 32 | "Programming Language :: Python :: 3.5", 33 | "Operating System :: POSIX", 34 | "Operating System :: POSIX :: Linux", 35 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 36 | "Topic :: Software Development :: Libraries :: Python Modules", 37 | ], 38 | install_requires=[ 39 | 'setuptools>=0.6b1', 40 | 'six>=1.11.0', 41 | 'Django>=1.8', 42 | ], 43 | ) 44 | -------------------------------------------------------------------------------- /static_sitemaps/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Reusable app meant to allow users to serve sitemap files by webserver 3 | instead of Django and generate sitemaps by running cron command. 4 | 5 | This results in better performance for sites with loads of content and/or URLS. 6 | 7 | App is dependent on original django.contrib.sitemaps application which 8 | has to be included in order for this to work. 9 | """ 10 | __versionstr__ = '5.0.0' 11 | -------------------------------------------------------------------------------- /static_sitemaps/conf.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | # Base sitemap config dict as stated in Django docs. 5 | ROOT_SITEMAP = settings.STATICSITEMAPS_ROOT_SITEMAP 6 | 7 | # Path to root location where the sitemaps will be stored. 8 | ROOT_DIR = getattr(settings, 'STATICSITEMAPS_ROOT_DIR', settings.STATIC_ROOT) 9 | 10 | # Compress the result? 11 | USE_GZIP = getattr(settings, 'STATICSITEMAPS_USE_GZIP', True) 12 | 13 | # How to compress it? Must be in ('python', 'system'). 14 | GZIP_METHOD = getattr(settings, 'STATICSITEMAPS_GZIP_METHOD', 'python') 15 | 16 | # Path to system gzip binary if system method is selected. 17 | SYSTEM_GZIP_PATH = getattr(settings, 'STATICSITEMAPS_SYSTEM_GZIP_PATH', '/usr/bin/gzip') 18 | 19 | # Template how to name the resulting sitemap pages. 20 | FILENAME_TEMPLATE = getattr(settings, 21 | 'STATICSITEMAPS_FILENAME_TEMPLATE', 22 | 'sitemap-%(section)s-%(page)s.xml') 23 | 24 | # Only for backwards compatibility, same as URL. 25 | DOMAIN = getattr(settings, 'STATICSITEMAPS_DOMAIN', None) 26 | 27 | # Language of sitemaps. 28 | LANGUAGE = getattr(settings, 'STATICSITEMAPS_LANGUAGE', settings.LANGUAGE_CODE) 29 | 30 | # Template for sitemap index. 31 | INDEX_TEMPLATE = getattr(settings, 'STATICSITEMAPS_INDEX_TEMPLATE', 32 | 'static_sitemaps/sitemap_index.xml') 33 | 34 | # Storage class to use. 35 | STORAGE_CLASS = getattr(settings, 'STATICSITEMAPS_STORAGE', 'django.core.files.storage.FileSystemStorage') 36 | 37 | # How often should the celery task be run. 38 | CELERY_TASK_REPETITION = getattr(settings, 'STATICSITEMAPS_REFRESH_AFTER', 60) 39 | 40 | # URL to serve sitemaps from. 41 | _url = getattr(settings, 'STATICSITEMAPS_URL', None) 42 | 43 | # Force the protocol to use with django sites framework 44 | FORCE_PROTOCOL = getattr(settings, 'STATICSITEMAPS_FORCE_PROTOCOL', None) 45 | 46 | # Mock django sites framework 47 | MOCK_SITE = getattr(settings, 'STATICSITEMAPS_MOCK_SITE', False) 48 | 49 | # Mock django sites framework with hostname string...for example www.yoursite.com 50 | MOCK_SITE_NAME = getattr(settings, 'STATICSITEMAPS_MOCK_SITE_NAME', None) 51 | 52 | # Mock django sites framework with https | https 53 | MOCK_SITE_PROTOCOL = getattr(settings, 'STATICSITEMAPS_MOCK_SITE_PROTOCOL', 'http') 54 | 55 | 56 | def get_url(): 57 | _url = getattr(settings, 'STATICSITEMAPS_URL', None) 58 | if _url is not None: 59 | return _url 60 | 61 | if DOMAIN: 62 | # Backwards compatibility. 63 | import warnings 64 | _url = DOMAIN 65 | warnings.warn('You are using STATICSITEMAPS_DOMAIN which is going to be ' 66 | 'deprecated soon. Please migrate to ' 67 | 'STATICSITEMAPS_URL', DeprecationWarning) 68 | elif settings.STATIC_URL.startswith('/'): 69 | # If STATIC_URL starts with '/', it is probably a relative URL to the 70 | # current domain so we append STATIC_URL. 71 | from django.contrib.sites.models import Site 72 | _url = Site.objects.get_current().domain + settings.STATIC_URL 73 | else: 74 | # If STATIC_URL starts with protocol, it is probably a special domain 75 | # for static files and we stick to it. 76 | _url = settings.STATIC_URL 77 | return _url 78 | -------------------------------------------------------------------------------- /static_sitemaps/generator.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | try: 4 | from cStringIO import StringIO 5 | except ImportError: 6 | from io import StringIO 7 | 8 | import gzip 9 | import hashlib 10 | import os 11 | import subprocess 12 | from six import BytesIO 13 | 14 | from django.core.exceptions import ImproperlyConfigured 15 | from django.core.files.base import ContentFile 16 | from django.core.files.storage import FileSystemStorage 17 | from django.core.paginator import EmptyPage, PageNotAnInteger 18 | 19 | try: 20 | from django.urls import reverse, NoReverseMatch 21 | except ImportError: # Django < 2.0 22 | from django.core.urlresolvers import reverse, NoReverseMatch 23 | 24 | from django.template import loader 25 | from django.utils import translation 26 | from django.utils.encoding import smart_str 27 | 28 | from static_sitemaps import conf 29 | from static_sitemaps.util import _lazy_load 30 | 31 | __author__ = 'xaralis' 32 | 33 | 34 | class SitemapGenerator(object): 35 | def __init__(self, verbosity): 36 | self.verbosity = verbosity 37 | try: 38 | self.storage = _lazy_load(conf.STORAGE_CLASS)(location=conf.ROOT_DIR) 39 | except TypeError: 40 | self.storage = _lazy_load(conf.STORAGE_CLASS)() 41 | 42 | self.sitemaps = _lazy_load(conf.ROOT_SITEMAP) 43 | 44 | if not isinstance(self.sitemaps, dict): 45 | self.sitemaps = dict(enumerate(self.sitemaps)) 46 | 47 | @staticmethod 48 | def get_hash(bytestream): 49 | return hashlib.md5(bytestream).digest() 50 | 51 | @staticmethod 52 | def normalize_url(url): 53 | if url[-1] != '/': 54 | url += '/' 55 | if not url.startswith(('http://', 'https://')): 56 | protocol = conf.FORCE_PROTOCOL or 'http' 57 | prefix = '%s://' % protocol 58 | if url.startswith('/'): 59 | from django.contrib.sites.models import Site 60 | url = prefix + Site.objects.get_current().domain + url 61 | else: 62 | url = prefix + url 63 | return url 64 | 65 | def _write(self, path, output): 66 | output = bytes(output, "utf8") # botoS3 has some issues with encoding in Python 3 67 | self.storage.save(path, ContentFile(output)) 68 | 69 | def read_hash(self, path): 70 | with self.storage.open(path) as f: 71 | result = self.get_hash(f.read()) 72 | return result 73 | 74 | def out(self, string, min_level=1): 75 | if self.verbosity >= min_level: 76 | print(string) 77 | 78 | def write(self): 79 | self.out('Generating sitemaps.', 1) 80 | translation.activate(conf.LANGUAGE) 81 | self.write_index() 82 | translation.deactivate() 83 | self.out('Finished generating sitemaps.', 1) 84 | 85 | def write_index(self): 86 | baseurl = self.normalize_url(conf.get_url()) 87 | parts = [] 88 | 89 | # Collect all pages and write them. 90 | for section, site in self.sitemaps.items(): 91 | if callable(site): 92 | pages = site().paginator.num_pages 93 | else: 94 | pages = site.paginator.num_pages 95 | 96 | for page in range(1, pages + 1): 97 | filename = conf.FILENAME_TEMPLATE % {'section': section, 98 | 'page': page} 99 | lastmod = self.write_page(site, page, filename) 100 | 101 | if conf.USE_GZIP: 102 | filename += '.gz' 103 | 104 | parts.append({ 105 | 'location': '%s%s' % (baseurl, filename), 106 | 'lastmod': lastmod 107 | }) 108 | 109 | path = os.path.join(conf.ROOT_DIR, 'sitemap.xml') 110 | self.out('Writing index file.', 2) 111 | 112 | if self.storage.exists(path): 113 | self.storage.delete(path) 114 | 115 | output = loader.render_to_string(conf.INDEX_TEMPLATE, {'sitemaps': parts}) 116 | self._write(path, output) 117 | 118 | def write_page(self, site, page, filename): 119 | self.out('Writing sitemap %s.' % filename, 2) 120 | old_page_md5 = None 121 | urls = [] 122 | 123 | if conf.MOCK_SITE: 124 | if conf.MOCK_SITE_NAME is None: 125 | raise ImproperlyConfigured("STATICSITEMAPS_MOCK_SITE_NAME must not be None. Try setting to www.yoursite.com") 126 | from django.contrib.sites.requests import RequestSite 127 | from django.test.client import RequestFactory 128 | rs = RequestSite(RequestFactory().get('/', SERVER_NAME=conf.MOCK_SITE_NAME)) 129 | try: 130 | if callable(site): 131 | if conf.MOCK_SITE: 132 | urls.extend(site().get_urls(page, rs, protocol=conf.MOCK_SITE_PROTOCOL)) 133 | else: 134 | urls.extend(site().get_urls(page, protocol=conf.FORCE_PROTOCOL)) 135 | else: 136 | if conf.MOCK_SITE: 137 | urls.extend(site.get_urls(page, rs, protocol=conf.MOCK_SITE_PROTOCOL)) 138 | else: 139 | urls.extend(site.get_urls(page, protocol=conf.FORCE_PROTOCOL)) 140 | except EmptyPage: 141 | self.out("Page %s empty" % page) 142 | except PageNotAnInteger: 143 | self.out("No page '%s'" % page) 144 | 145 | lastmods = [lastmod for lastmod in [u.get('lastmod') for u in urls] if lastmod is not None] 146 | file_lastmod = max(lastmods) if len(lastmods) > 0 else None 147 | path = os.path.join(conf.ROOT_DIR, filename) 148 | template = getattr(site, 'sitemap_template', 'sitemap.xml') 149 | 150 | if self.storage.exists(path): 151 | old_page_md5 = self.read_hash(path) 152 | self.storage.delete(path) 153 | 154 | output = smart_str(loader.render_to_string(template, {'urlset': urls})) 155 | self._write(path, output) 156 | 157 | if conf.USE_GZIP: 158 | if conf.GZIP_METHOD not in ['python', 'system', ]: 159 | raise ImproperlyConfigured("STATICSITEMAPS_GZIP_METHOD must be in ['python', 'system']") 160 | 161 | if conf.GZIP_METHOD == 'system' and not os.path.exists(conf.SYSTEM_GZIP_PATH): 162 | raise ImproperlyConfigured('STATICSITEMAPS_SYSTEM_GZIP_PATH does not exist') 163 | 164 | if conf.GZIP_METHOD == 'system' and not isinstance(self.storage, FileSystemStorage): 165 | raise ImproperlyConfigured('system gzip method can only be used with FileSystemStorage') 166 | 167 | if conf.GZIP_METHOD == 'system': 168 | # GZIP with system gzip binary 169 | subprocess.call([conf.SYSTEM_GZIP_PATH, '-f', path, ]) 170 | else: 171 | # GZIP with python gzip lib 172 | try: 173 | gzipped_path = '%s.gz' % path 174 | if self.storage.exists(gzipped_path): 175 | self.storage.delete(gzipped_path) 176 | 177 | self.out('Compressing...', 2) 178 | buf = BytesIO() 179 | with gzip.GzipFile(fileobj=buf, mode="w") as f: 180 | f.write(output.encode('utf-8')) 181 | self.storage.save(gzipped_path, ContentFile(buf.getvalue())) 182 | except OSError: 183 | self.out("Compress %s file error" % path) 184 | 185 | return file_lastmod 186 | -------------------------------------------------------------------------------- /static_sitemaps/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xaralis/django-static-sitemaps/b514be87edf07b03f70bd0945615177c7326c2dd/static_sitemaps/management/__init__.py -------------------------------------------------------------------------------- /static_sitemaps/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xaralis/django-static-sitemaps/b514be87edf07b03f70bd0945615177c7326c2dd/static_sitemaps/management/commands/__init__.py -------------------------------------------------------------------------------- /static_sitemaps/management/commands/refresh_sitemap.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from static_sitemaps.generator import SitemapGenerator 3 | 4 | __author__ = 'xaralis' 5 | 6 | 7 | class Command(BaseCommand): 8 | command = None 9 | help = 'Generates sitemaps files to a predefined directory.' 10 | 11 | def handle(self, *args, **options): 12 | generator = SitemapGenerator(int(options.get('verbosity', 0))) 13 | generator.write() 14 | -------------------------------------------------------------------------------- /static_sitemaps/tasks.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from celery import Task 4 | 5 | from static_sitemaps import conf 6 | from static_sitemaps.generator import SitemapGenerator 7 | 8 | __author__ = 'xaralis' 9 | 10 | # Create class conditionally so the task can be bypassed when repetition 11 | # is set to something which evaluates to False. 12 | if conf.CELERY_TASK_REPETITION: 13 | class GenerateSitemap(Task): 14 | run_every = timedelta(minutes=conf.CELERY_TASK_REPETITION) 15 | 16 | def run(self, **kwargs): 17 | generator = SitemapGenerator(verbosity=1) 18 | generator.write() 19 | 20 | -------------------------------------------------------------------------------- /static_sitemaps/templates/static_sitemaps/sitemap_index.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% spaceless %} 4 | {% for site in sitemaps %} 5 | 6 | {{ site.location }} 7 | {% if site.lastmod %} 8 | {{ site.lastmod|date:"Y-m-d\TH:i:sO"|slice:":-2" }}:00 9 | {% endif %} 10 | 11 | {% endfor %} 12 | {% endspaceless %} 13 | 14 | -------------------------------------------------------------------------------- /static_sitemaps/urls.py: -------------------------------------------------------------------------------- 1 | from static_sitemaps.views import SitemapView 2 | 3 | try: 4 | from django.urls import path, re_path 5 | 6 | urlpatterns = [ 7 | path('sitemap.xml', SitemapView.as_view(), kwargs={'section': 'sitemap'}, name='static_sitemaps_index'), 8 | re_path(r'^(?P
sitemap-.+)\.xml$', SitemapView.as_view()), 9 | ] 10 | except ImportError: # Django < 2.0 11 | from django.conf.urls import url 12 | 13 | urlpatterns = [ 14 | url(r'^sitemap\.xml$', SitemapView.as_view(), kwargs={'section': 'sitemap'}, name='static_sitemaps_index'), 15 | url(r'^(?P
sitemap-.+)\.xml$', SitemapView.as_view()), 16 | ] 17 | -------------------------------------------------------------------------------- /static_sitemaps/util.py: -------------------------------------------------------------------------------- 1 | from importlib import import_module 2 | 3 | from django.core.exceptions import ImproperlyConfigured 4 | 5 | 6 | def _lazy_load(class_path): 7 | module, attr = class_path.rsplit('.', 1) 8 | try: 9 | mod = import_module(module) 10 | except ImportError as e: 11 | raise ImproperlyConfigured('Error importing module %s: "%s"' % 12 | (module, e)) 13 | try: 14 | instance = getattr(mod, attr) 15 | return instance 16 | except AttributeError: 17 | raise ImproperlyConfigured('Module "%s" does not define a "%s" ' 18 | 'class.' % (module, attr)) 19 | -------------------------------------------------------------------------------- /static_sitemaps/views.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.http import HttpResponse, Http404 4 | from django.views.generic import View 5 | 6 | from static_sitemaps import conf 7 | from static_sitemaps.util import _lazy_load 8 | 9 | 10 | class SitemapView(View): 11 | def get(self, request, *args, **kwargs): 12 | section = kwargs.get('section') 13 | 14 | try: 15 | storage = _lazy_load(conf.STORAGE_CLASS)(location=conf.ROOT_DIR) 16 | except TypeError: 17 | storage = _lazy_load(conf.STORAGE_CLASS)() 18 | 19 | path = os.path.join(conf.ROOT_DIR, '{}.xml'.format(section)) 20 | if not storage.exists(path): 21 | raise Http404('No sitemap file found on %r. Run django-admin.py ' 22 | 'refresh_sitemap first.' % path) 23 | 24 | f = storage.open(path) 25 | content = f.readlines() 26 | f.close() 27 | return HttpResponse(content, content_type='application/xml') 28 | -------------------------------------------------------------------------------- /testapp/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == '__main__': 6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testapp.settings') 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /testapp/sitemaps/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !.keep 4 | -------------------------------------------------------------------------------- /testapp/testapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xaralis/django-static-sitemaps/b514be87edf07b03f70bd0945615177c7326c2dd/testapp/testapp/__init__.py -------------------------------------------------------------------------------- /testapp/testapp/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for testapp project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.1/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'h5*8vproll6#h%mps_zo8^x)u*$8gli6@36sd-b-c%axqiv8g-' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'django.contrib.sites', 41 | 'django.contrib.flatpages', 42 | 'django.contrib.sitemaps', 43 | 44 | 'static_sitemaps', 45 | ] 46 | 47 | MIDDLEWARE = [ 48 | 'django.middleware.security.SecurityMiddleware', 49 | 'django.contrib.sessions.middleware.SessionMiddleware', 50 | 'django.middleware.common.CommonMiddleware', 51 | 'django.middleware.csrf.CsrfViewMiddleware', 52 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 53 | 'django.contrib.messages.middleware.MessageMiddleware', 54 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 55 | 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', 56 | ] 57 | 58 | ROOT_URLCONF = 'testapp.urls' 59 | 60 | TEMPLATES = [ 61 | { 62 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 63 | 'DIRS': [], 64 | 'APP_DIRS': True, 65 | 'OPTIONS': { 66 | 'context_processors': [ 67 | 'django.template.context_processors.debug', 68 | 'django.template.context_processors.request', 69 | 'django.contrib.auth.context_processors.auth', 70 | 'django.contrib.messages.context_processors.messages', 71 | ], 72 | }, 73 | }, 74 | ] 75 | 76 | WSGI_APPLICATION = 'testapp.wsgi.application' 77 | 78 | 79 | # Database 80 | # https://docs.djangoproject.com/en/2.1/ref/settings/#databases 81 | 82 | DATABASES = { 83 | 'default': { 84 | 'ENGINE': 'django.db.backends.sqlite3', 85 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 86 | } 87 | } 88 | 89 | 90 | # Password validation 91 | # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators 92 | 93 | AUTH_PASSWORD_VALIDATORS = [ 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 102 | }, 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 105 | }, 106 | ] 107 | 108 | 109 | # Internationalization 110 | # https://docs.djangoproject.com/en/2.1/topics/i18n/ 111 | 112 | LANGUAGE_CODE = 'en-us' 113 | 114 | TIME_ZONE = 'UTC' 115 | 116 | USE_I18N = True 117 | 118 | USE_L10N = True 119 | 120 | USE_TZ = True 121 | 122 | 123 | # Static files (CSS, JavaScript, Images) 124 | # https://docs.djangoproject.com/en/2.1/howto/static-files/ 125 | 126 | STATIC_URL = '/static/' 127 | 128 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 129 | STATICSITEMAPS_ROOT_DIR = os.path.join(BASE_DIR, 'sitemaps') 130 | 131 | STATICFILES_DIRS = [ 132 | STATICSITEMAPS_ROOT_DIR, 133 | ] 134 | 135 | SITE_ID = 1 136 | 137 | STATICSITEMAPS_ROOT_SITEMAP = 'testapp.sitemaps.sitemaps' 138 | -------------------------------------------------------------------------------- /testapp/testapp/sitemaps.py: -------------------------------------------------------------------------------- 1 | from django.contrib.flatpages.sitemaps import FlatPageSitemap 2 | 3 | sitemaps = { 4 | 'flatpages': FlatPageSitemap, 5 | } 6 | -------------------------------------------------------------------------------- /testapp/testapp/urls.py: -------------------------------------------------------------------------------- 1 | """testapp URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | from django.conf.urls.static import static 19 | from django.conf import settings 20 | 21 | urlpatterns = [ 22 | path('admin/', admin.site.urls), 23 | path('pages/', include('django.contrib.flatpages.urls')), 24 | path('', include('static_sitemaps.urls')), 25 | ] + static(prefix=settings.STATIC_URL, show_indexes=True) 26 | -------------------------------------------------------------------------------- /testapp/testapp/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for testapp project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testapp.settings') 15 | 16 | application = get_wsgi_application() 17 | --------------------------------------------------------------------------------