@@ -736,6 +736,107 @@ class ServeRobotsTXT(SettingsOverrideObject):
736736 _default_class = ServeRobotsTXTBase
737737
738738
739+ class ServeLLMSTXTBase (CDNCacheControlMixin , CDNCacheTagsMixin , ServeDocsMixin , View ):
740+ """Serve llms.txt from the domain's root."""
741+
742+ cache_response = True
743+ project_cache_tag = "llms.txt"
744+
745+ def get (self , request ):
746+ """
747+ Serve custom user's defined ``/llms.txt``.
748+
749+ If the project is delisted or is a spam project, we force a special llms.txt.
750+
751+ If the user added a ``llms.txt`` in the "default version" of the
752+ project, we serve it directly.
753+ """
754+
755+ project = request .unresolved_domain .project
756+
757+ if project .delisted :
758+ return render (
759+ request ,
760+ "llms.delisted.txt" ,
761+ content_type = "text/plain" ,
762+ )
763+
764+ if "readthedocsext.spamfighting" in settings .INSTALLED_APPS :
765+ from readthedocsext .spamfighting .utils import is_robotstxt_denied # noqa
766+
767+ if is_robotstxt_denied (project ):
768+ return render (
769+ request ,
770+ "llms.spam.txt" ,
771+ content_type = "text/plain" ,
772+ )
773+
774+ version_slug = project .get_default_version ()
775+ version = project .versions .get (slug = version_slug )
776+
777+ no_serve_llms_txt = any (
778+ [
779+ version .privacy_level == PRIVATE ,
780+ not version .active ,
781+ not version .built ,
782+ ]
783+ )
784+
785+ if no_serve_llms_txt :
786+ raise Http404 ()
787+
788+ structlog .contextvars .bind_contextvars (
789+ project_slug = project .slug ,
790+ version_slug = version .slug ,
791+ )
792+
793+ try :
794+ response = self ._serve_docs (
795+ request = request ,
796+ project = project ,
797+ version = version ,
798+ filename = "llms.txt" ,
799+ check_if_exists = True ,
800+ )
801+ log .info ("Serving custom llms.txt file." )
802+ return response
803+ except StorageFileNotFound :
804+ pass
805+
806+ sitemap_url = "{scheme}://{domain}/sitemap.xml" .format (
807+ scheme = "https" ,
808+ domain = project .subdomain (),
809+ )
810+ context = {
811+ "sitemap_url" : sitemap_url ,
812+ "hidden_paths" : self ._get_hidden_paths (project ),
813+ }
814+ return render (
815+ request ,
816+ "llms.txt" ,
817+ context ,
818+ content_type = "text/plain" ,
819+ )
820+
821+ def _get_hidden_paths (self , project ):
822+ hidden_versions = project .versions (manager = INTERNAL ).public ().filter (hidden = True )
823+ resolver = Resolver ()
824+ hidden_paths = [
825+ resolver .resolve_path (project , version_slug = version .slug ) for version in hidden_versions
826+ ]
827+ return hidden_paths
828+
829+ def _get_project (self ):
830+ return self .request .unresolved_domain .project
831+
832+ def _get_version (self ):
833+ return None
834+
835+
836+ class ServeLLMSTXT (SettingsOverrideObject ):
837+ _default_class = ServeLLMSTXTBase
838+
839+
739840class ServeSitemapXMLBase (CDNCacheControlMixin , CDNCacheTagsMixin , View ):
740841 """Serve sitemap.xml from the domain's root."""
741842
0 commit comments