Repository: django/django
Issue category: Feature Request

Issue:
Improve non-first {% extends %} error message
Description
	 
Currently if you put {% extends %} after another tag, the error looks like:
TemplateSyntaxError: <ExtendsNode: extends 'base.html'> must be the first tag in the template.
ExtendsNode is a leaked internal detail. Showing its repr() is a bit confusing, especially for a beginner programmer.
The message would be clearer if it showed the actual text of the tag:
TemplateSyntaxError: {% extends 'base.html' %} must be the first tag in the template.


Hints:
['So After looking at the code I am a bit stuck on what should be done, the error message is constructed using the __repr__ method of ExtendNode class # The way the error is constructed if node.must_be_first and nodelist.contains_nontext: raise self.error( token, "%r must be the first tag in the template." % node, ) And this is the __repr__ function # __repr__ function for ExtendsNode class def __repr__(self): return "<%s: extends %s>" % (self.__class__.__name__, self.parent_name.token) I don\'t think changing the repr method or hard coding the error message is any better. So what should be done ?', 1727505575.0]
['The repr should not be changed, it’s still used in other contexts like debugging. Change the error message to show/build the extends tag source.', 1727511576.0]
["Just sent a PR to github to add a clear error message. \u200bPR Edit: I didn't notice the first PR, sorry about that.", 1728200226.0]

Patch:
diff --git a/django/template/base.py b/django/template/base.py
index ee2e145c041a..b974495c9c92 100644
--- a/django/template/base.py
+++ b/django/template/base.py
@@ -533,9 +533,13 @@ def skip_past(self, endtag):
     def extend_nodelist(self, nodelist, node, token):
         # Check that non-text nodes don't appear before an extends tag.
         if node.must_be_first and nodelist.contains_nontext:
+            if self.origin.template_name:
+                origin = repr(self.origin.template_name)
+            else:
+                origin = "the template"
             raise self.error(
                 token,
-                "%r must be the first tag in the template." % node,
+                "{%% %s %%} must be the first tag in %s." % (token.contents, origin),
             )
         if not isinstance(node, TextNode):
             nodelist.contains_nontext = True


Test patch:
diff --git a/tests/template_tests/test_extends.py b/tests/template_tests/test_extends.py
index ce1838654bac..0d2a93468ce3 100644
--- a/tests/template_tests/test_extends.py
+++ b/tests/template_tests/test_extends.py
@@ -1,9 +1,9 @@
 import os
 
-from django.template import Context, Engine, TemplateDoesNotExist
+from django.template import Context, Engine, TemplateDoesNotExist, TemplateSyntaxError
 from django.test import SimpleTestCase
 
-from .utils import ROOT
+from .utils import ROOT, setup
 
 RECURSIVE = os.path.join(ROOT, "recursive_templates")
 
@@ -181,3 +181,17 @@ def test_block_override_in_extended_included_template(self):
         )
         template = engine.get_template("base.html")
         self.assertEqual(template.render(Context({})), "12AB")
+
+    @setup(
+        {"index.html": "{% block content %}B{% endblock %}{% extends 'base.html' %}"}
+    )
+    def test_extends_not_first_tag_in_extended_template(self):
+        msg = "{% extends 'base.html' %} must be the first tag in 'index.html'."
+        with self.assertRaisesMessage(TemplateSyntaxError, msg):
+            self.engine.get_template("index.html")
+
+    def test_extends_not_first_tag_in_extended_template_from_string(self):
+        template_string = "{% block content %}B{% endblock %}{% extends 'base.html' %}"
+        msg = "{% extends 'base.html' %} must be the first tag in the template."
+        with self.assertRaisesMessage(TemplateSyntaxError, msg):
+            Engine().from_string(template_string)