Repository: orhun/git-cliff (rust)

Issue:
Enable having a footer like the KeepAChangelog one
**Is your feature request related to a problem? Please describe.**
In my changelogs, I would like to have a footer like the one of [KeepAChangelog](https://keepachangelog.com/en/1.1.0/):
```
[Unreleased]: https://github.com/olivierlacan/keep-a-changelog/compare/v1.0.0...HEAD
[1.0.0]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.3.0...v1.0.0
[0.3.0]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.2.0...v0.3.0
```

This enables you to click on the links of each entry of the changelog to see the changes.

I think this isn't possible to achieve at the moment with `git-cliff`.

Have you ever thought about supporting such a use case?

**Describe the solution you'd like**

There should be a way for git-cliff to update the footer of the changelog together with the body.

**Describe alternatives you've considered**
git-cliff could have a new entry in the configuration: `version_link`, in which the user can specify a template to add after the `[<version>]:` row.

For example, in the configuration you can have something like:
```toml
version_link = """
{% if previous %}\
{% if previous.version %}\
    https://github.com/olivierlacan/keep-a-changelog/compare/{{ previous.version }}...{{ version }}
{% else %}\
    https://github.com/olivierlacan/keep-a-changelog/releases/tag/{{ version }}
{% endif %}\
{% endif %}\
"""
```

Then, git-cliff takes care of updating the versions at the end of the changelog.

Maybe you could also have a `unreleased_link`, because the template in this case is different:
```toml
unreleased_link = """
    https://github.com/olivierlacan/keep-a-changelog/compare/{{ version }}...HEAD
"""
```

**Additional context**
Enabling "templated footers" isn't enough because every time you add a new entry to the changelog, you need to edit the `[Unreleased]` row with the latest version.

For example, the footer for the 0.3.0 version looks like this:
```
[Unreleased]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.3.0...HEAD
[0.3.0]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.2.0...v0.3.0
```

Instead the footer of the 1.0.0 version looks like this:
```
[Unreleased]: https://github.com/olivierlacan/keep-a-changelog/compare/v1.0.0...HEAD
[1.0.0]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.3.0...v1.0.0
[0.3.0]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.2.0...v0.3.0
```

As you can see we have both added the `[1.0.0]` row, and edited the `[Unreleased]` one.

Pull request description:
feat(changelog)!: support templating in the footer
## Description

This PR makes it possible to use a template for the footer.

Before:

```toml
# changelog footer
footer = """
<!-- generated by git-cliff -->
"""
```

After:

```toml
# template for the changelog footer
footer = """
<!-- generated by git-cliff at {{ now() | date(format="%Y-%m-%d") }} -->
"""
```

## Motivation and Context

Now we can have links at the footer for having the exact format as [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) specification:

```toml
# template for the changelog footer
footer = """
{% for release in releases %}\
    {% if release.version %}\
        {% if release.previous.version %}\
            [{{ release.version | trim_start_matches(pat="v") }}]: <REPO>/compare/{{ release.previous.version }}..{{ release.version }}
        {% endif %}\
    {% else %}\
        [unreleased]: <REPO>/compare/{{ release.previous.version }}..HEAD
    {% endif %}\
{% endfor %}
<!-- generated by git-cliff -->
"""
```

Closes #95

## How Has This Been Tested?

- Unit tests
- Integration tests
- Test fixtures

## Types of Changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [x] Breaking change (fix or feature that would cause existing functionality to change)
- [x] Documentation (no code change)
- [x] Refactor (refactoring production code)
- [ ] Other <!--- (provide information) -->

## Checklist:

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [x] My code follows the code style of this project.
- [x] I have updated the documentation accordingly.
- [x] I have formatted the code with [rustfmt](https://github.com/rust-lang/rustfmt).
- [x] I checked the lints with [clippy](https://github.com/rust-lang/rust-clippy).
- [x] I have added tests to cover my changes.
- [x] All new and existing tests passed.


Patch:
diff --git a/cliff.toml b/cliff.toml
index a9fca24..b894fe3 100644
--- a/cliff.toml
+++ b/cliff.toml
@@ -44,12 +44,12 @@ body = """
     {% endfor -%}
 {% endfor %}\n
 """
-# remove the leading and trailing whitespace from the template
-trim = true
-# changelog footer
+# template for the changelog footer
 footer = """
 <!-- generated by git-cliff -->
 """
+# remove the leading and trailing whitespace from the templates
+trim = true
 # postprocessors
 postprocessors = [
   { pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL
diff --git a/config/cliff.toml b/config/cliff.toml
index bd46489..86933d2 100644
--- a/config/cliff.toml
+++ b/config/cliff.toml
@@ -26,12 +26,12 @@ body = """
     {% endfor %}
 {% endfor %}\n
 """
-# remove the leading and trailing whitespace from the template
-trim = true
-# changelog footer
+# template for the changelog footer
 footer = """
 <!-- generated by git-cliff -->
 """
+# remove the leading and trailing s
+trim = true
 # postprocessors
 postprocessors = [
   # { pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL
diff --git a/examples/cocogitto.toml b/examples/cocogitto.toml
index fae1952..d6f47d6 100644
--- a/examples/cocogitto.toml
+++ b/examples/cocogitto.toml
@@ -38,12 +38,12 @@ body = """
     {% endfor -%}
 {% endfor %}\n
 """
-# remove the leading and trailing whitespace from the template
-trim = true
-# changelog footer
+# template for the changelog footer
 footer = """
 <!-- generated by git-cliff -->
 """
+# remove the leading and trailing whitespace from the templates
+trim = true
 # postprocessors
 postprocessors = [
     { pattern = '\$REPO', replace = "https://github.com/cocogitto/cocogitto" }, # replace repository URL
diff --git a/examples/detailed.toml b/examples/detailed.toml
index 50d2138..f9f5d7d 100644
--- a/examples/detailed.toml
+++ b/examples/detailed.toml
@@ -31,12 +31,12 @@ body = """
     {% endfor %}
 {% endfor %}\n
 """
-# remove the leading and trailing whitespace from the template
-trim = true
-# changelog footer
+# template for the changelog footer
 footer = """
 <!-- generated by git-cliff -->
 """
+# remove the leading and trailing whitespace from the templates
+trim = true
 
 [git]
 # parse the commits based on https://www.conventionalcommits.org
diff --git a/examples/keepachangelog.toml b/examples/keepachangelog.toml
index 7814bea..8d9e244 100644
--- a/examples/keepachangelog.toml
+++ b/examples/keepachangelog.toml
@@ -25,12 +25,26 @@ body = """
     {% endfor %}
 {% endfor %}\n
 """
-# remove the leading and trailing whitespace from the template
-trim = true
-# changelog footer
+# template for the changelog footer
 footer = """
+{% for release in releases %}\
+    {% if release.version %}\
+        {% if release.previous.version %}\
+            [{{ release.version | trim_start_matches(pat="v") }}]: <REPO>/compare/{{ release.previous.version }}..{{ release.version }}
+        {% endif %}\
+    {% else %}\
+        [unreleased]: <REPO>/compare/{{ release.previous.version }}..HEAD
+    {% endif %}\
+{% endfor %}
 <!-- generated by git-cliff -->
 """
+# remove the leading and trailing whitespace from the templates
+trim = true
+# postprocessors
+postprocessors = [
+  # replace repository URL
+  { pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" },
+]
 
 [git]
 # parse the commits based on https://www.conventionalcommits.org
@@ -41,14 +55,14 @@ filter_unconventional = true
 split_commits = false
 # regex for parsing and grouping commits
 commit_parsers = [
-    { message = "^.*: add", group = "Added" },
-    { message = "^.*: support", group = "Added" },
-    { message = "^.*: remove", group = "Removed" },
-    { message = "^.*: delete", group = "Removed" },
-    { message = "^test", group = "Fixed" },
-    { message = "^fix", group = "Fixed" },
-    { message = "^.*: fix", group = "Fixed" },
-    { message = "^.*", group = "Changed" },
+  { message = "^.*: add", group = "Added" },
+  { message = "^.*: support", group = "Added" },
+  { message = "^.*: remove", group = "Removed" },
+  { message = "^.*: delete", group = "Removed" },
+  { message = "^test", group = "Fixed" },
+  { message = "^fix", group = "Fixed" },
+  { message = "^.*: fix", group = "Fixed" },
+  { message = "^.*", group = "Changed" },
 ]
 # protect breaking changes from being skipped due to matching a skipping commit_parser
 protect_breaking_commits = false
diff --git a/examples/scoped.toml b/examples/scoped.toml
index 91bc81b..46c06e3 100644
--- a/examples/scoped.toml
+++ b/examples/scoped.toml
@@ -25,12 +25,12 @@ body = """
     {% endfor %}\
 {% endfor %}\n
 """
-# remove the leading and trailing whitespace from the template
-trim = true
-# changelog footer
+# template for the changelog footer
 footer = """
 <!-- generated by git-cliff -->
 """
+# remove the leading and trailing whitespace from the templates
+trim = true
 
 [git]
 # parse the commits based on https://www.conventionalcommits.org
diff --git a/examples/scopesorted.toml b/examples/scopesorted.toml
index bc3b8ad..ce13531 100644
--- a/examples/scopesorted.toml
+++ b/examples/scopesorted.toml
@@ -38,12 +38,13 @@ body = """
     {% raw %}\n{% endraw %}\
 {% endfor %}\n
 """
-# remove the leading and trailing whitespace from the template
-trim = true
-# changelog footer
+# template for the changelog footer
 footer = """
 <!-- generated by git-cliff -->
 """
+# remove the leading and trailing whitespace from the templates
+trim = true
+
 
 [git]
 # parse the commits based on https://www.conventionalcommits.org
diff --git a/examples/unconventional.toml b/examples/unconventional.toml
index f44894e..0a255cf 100644
--- a/examples/unconventional.toml
+++ b/examples/unconventional.toml
@@ -22,12 +22,13 @@ body = """
     {% endfor %}
 {% endfor %}\n
 """
-# remove the leading and trailing whitespace from the template
-trim = true
-# changelog footer
+# template for the changelog footer
 footer = """
 <!-- generated by git-cliff -->
 """
+# remove the leading and trailing whitespace from the templates
+trim = true
+
 
 [git]
 # parse the commits based on https://www.conventionalcommits.org
diff --git a/git-cliff-core/src/changelog.rs b/git-cliff-core/src/changelog.rs
index fc1553c..29569c3 100644
--- a/git-cliff-core/src/changelog.rs
+++ b/git-cliff-core/src/changelog.rs
@@ -11,30 +11,31 @@ use std::io::Write;
 /// Changelog generator.
 #[derive(Debug)]
 pub struct Changelog<'a> {
-	releases: Vec<Release<'a>>,
-	template: Template,
-	config:   &'a Config,
+	releases:        Vec<Release<'a>>,
+	body_template:   Template,
+	footer_template: Option<Template>,
+	config:          &'a Config,
 }
 
 impl<'a> Changelog<'a> {
 	/// Constructs a new instance.
 	pub fn new(releases: Vec<Release<'a>>, config: &'a Config) -> Result<Self> {
-		let mut template = config
-			.changelog
-			.body
-			.as_deref()
-			.unwrap_or_default()
-			.to_string();
-		if config.changelog.trim.unwrap_or(true) {
-			template = template
-				.lines()
-				.map(|v| v.trim())
-				.collect::<Vec<&str>>()
-				.join("\n")
-		}
+		let trim = config.changelog.trim.unwrap_or(true);
 		let mut changelog = Self {
 			releases,
-			template: Template::new(template)?,
+			body_template: Template::new(
+				config
+					.changelog
+					.body
+					.as_deref()
+					.unwrap_or_default()
+					.to_string(),
+				trim,
+			)?,
+			footer_template: match &config.changelog.footer {
+				Some(footer) => Some(Template::new(footer.to_string(), trim)?),
+				None => None,
+			},
 			config,
 		};
 		changelog.process_commits();
@@ -141,19 +142,30 @@ impl<'a> Changelog<'a> {
 		if let Some(header) = &self.config.changelog.header {
 			write!(out, "{header}")?;
 		}
+		let postprocessors = self
+			.config
+			.changelog
+			.postprocessors
+			.clone()
+			.unwrap_or_default();
 		for release in &self.releases {
-			let mut rendered = self.template.render(release)?;
-			if let Some(postprocessors) =
-				self.config.changelog.postprocessors.as_ref()
-			{
-				for postprocessor in postprocessors {
-					postprocessor.replace(&mut rendered, vec![])?;
-				}
-			}
-			write!(out, "{}", rendered)?;
+			write!(
+				out,
+				"{}",
+				self.body_template.render(release, &postprocessors)?
+			)?;
 		}
-		if let Some(footer) = &self.config.changelog.footer {
-			write!(out, "{footer}")?;
+		if let Some(footer_template) = &self.footer_template {
+			writeln!(
+				out,
+				"{}",
+				footer_template.render(
+					&Releases {
+						releases: &self.releases,
+					},
+					&postprocessors,
+				)?
+			)?;
 		}
 		Ok(())
 	}
@@ -175,7 +187,10 @@ impl<'a> Changelog<'a> {
 
 	/// Prints the changelog context to the given output.
 	pub fn write_context<W: Write>(&self, out: &mut W) -> Result<()> {
-		let output = Releases(&self.releases).as_json()?;
+		let output = Releases {
+			releases: &self.releases,
+		}
+		.as_json()?;
 		writeln!(out, "{output}")?;
 		Ok(())
 	}
@@ -209,7 +224,9 @@ mod test {
 				- {{ commit.message }}{% endfor %}
 				{% endfor %}{% endfor %}"#,
 				)),
-				footer:         Some(String::from("------------")),
+				footer:         Some(String::from(
+					r#"-- total releases: {{ releases | length }} --"#,
+				)),
 				trim:           Some(true),
 				postprocessors: Some(vec![TextProcessor {
 					pattern:         Regex::new("boring")
@@ -468,7 +485,8 @@ mod test {
 
 			#### ui
 			- make good stuff
-			------------"#
+			-- total releases: 2 --
+			"#
 			)
 			.replace("			", ""),
 			str::from_utf8(&out).unwrap_or_default()
@@ -583,7 +601,8 @@ chore(deps): fix broken deps
 
 			#### ui
 			- make good stuff
-			------------"#
+			-- total releases: 2 --
+			"#
 			)
 			.replace("			", ""),
 			str::from_utf8(&out).unwrap_or_default()
diff --git a/git-cliff-core/src/release.rs b/git-cliff-core/src/release.rs
index a7d6d4b..92808b2 100644
--- a/git-cliff-core/src/release.rs
+++ b/git-cliff-core/src/release.rs
@@ -48,12 +48,16 @@ impl<'a> Release<'a> {
 }
 
 /// Representation of a list of releases.
-pub struct Releases<'a>(pub &'a Vec<Release<'a>>);
+#[derive(Serialize)]
+pub struct Releases<'a> {
+	/// Releases.
+	pub releases: &'a Vec<Release<'a>>,
+}
 
 impl<'a> Releases<'a> {
 	/// Returns the list of releases as JSON.
 	pub fn as_json(&self) -> Result<String> {
-		Ok(serde_json::to_string(self.0)?)
+		Ok(serde_json::to_string(self.releases)?)
 	}
 }
 
diff --git a/git-cliff-core/src/template.rs b/git-cliff-core/src/template.rs
index 4b94c47..c39f021 100644
--- a/git-cliff-core/src/template.rs
+++ b/git-cliff-core/src/template.rs
@@ -1,8 +1,11 @@
-use crate::error::{
-	Error,
-	Result,
+use crate::{
+	config::TextProcessor,
+	error::{
+		Error,
+		Result,
+	},
 };
-use crate::release::Release;
+use serde::Serialize;
 use std::collections::HashMap;
 use std::error::Error as ErrorImpl;
 use tera::{
@@ -20,7 +23,14 @@ pub struct Template {
 
 impl Template {
 	/// Constructs a new instance.
-	pub fn new(template: String) -> Result<Self> {
+	pub fn new(mut template: String, trim: bool) -> Result<Self> {
+		if trim {
+			template = template
+				.lines()
+				.map(|v| v.trim())
+				.collect::<Vec<&str>>()
+				.join("\n")
+		}
 		let mut tera = Tera::default();
 		if let Err(e) = tera.add_raw_template("template", &template) {
 			return if let Some(error_source) = e.source() {
@@ -49,10 +59,19 @@ impl Template {
 	}
 
 	/// Renders the template.
-	pub fn render(&self, release: &Release) -> Result<String> {
-		let context = TeraContext::from_serialize(release)?;
+	pub fn render<T: Serialize>(
+		&self,
+		context: &T,
+		postprocessors: &[TextProcessor],
+	) -> Result<String> {
+		let context = TeraContext::from_serialize(context)?;
 		match self.tera.render("template", &context) {
-			Ok(v) => Ok(v),
+			Ok(mut v) => {
+				for postprocessor in postprocessors {
+					postprocessor.replace(&mut v, vec![])?;
+				}
+				Ok(v)
+			}
 			Err(e) => {
 				return if let Some(error_source) = e.source() {
 					Err(Error::TemplateRenderError(error_source.to_string()))
@@ -68,19 +87,21 @@ impl Template {
 mod test {
 	use super::*;
 	use crate::commit::Commit;
+	use crate::release::Release;
+	use regex::Regex;
 
 	#[test]
 	fn render_template() -> Result<()> {
 		let template = r#"
-		## {{ version }}
+		## {{ version }} - <DATE>
 		{% for commit in commits %}
 		### {{ commit.group }}
 		- {{ commit.message | upper_first }}
 		{% endfor %}"#;
-		let template = Template::new(template.to_string())?;
+		let template = Template::new(template.to_string(), false)?;
 		assert_eq!(
 			r#"
-		## 1.0
+		## 1.0 - 2023
 		
 		### feat
 		- Add xyz
@@ -88,25 +109,33 @@ mod test {
 		### fix
 		- Fix abc
 		"#,
-			template.render(&Release {
-				version:   Some(String::from("1.0")),
-				commits:   vec![
-					Commit::new(
-						String::from("123123"),
-						String::from("feat(xyz): add xyz"),
-					),
-					Commit::new(
-						String::from("124124"),
-						String::from("fix(abc): fix abc"),
-					)
-				]
-				.into_iter()
-				.filter_map(|c| c.into_conventional().ok())
-				.collect(),
-				commit_id: None,
-				timestamp: 0,
-				previous:  None,
-			})?
+			template.render(
+				&Release {
+					version:   Some(String::from("1.0")),
+					commits:   vec![
+						Commit::new(
+							String::from("123123"),
+							String::from("feat(xyz): add xyz"),
+						),
+						Commit::new(
+							String::from("124124"),
+							String::from("fix(abc): fix abc"),
+						)
+					]
+					.into_iter()
+					.filter_map(|c| c.into_conventional().ok())
+					.collect(),
+					commit_id: None,
+					timestamp: 0,
+					previous:  None,
+				},
+				&[TextProcessor {
+					pattern:         Regex::new("<DATE>")
+						.expect("failed to compile regex"),
+					replace:         Some(String::from("2023")),
+					replace_command: None,
+				}]
+			)?
 		);
 		Ok(())
 	}
diff --git a/website/docs/configuration.md b/website/docs/configuration.md
index 187f5b0..0d72867 100644
--- a/website/docs/configuration.md
+++ b/website/docs/configuration.md
@@ -47,16 +47,22 @@ Body template that represents a single release in the changelog.
 
 See [templating](/docs/category/templating) for more detail.
 
+### footer
+
+Footer template that will be rendered and added to the end of the changelog.
+
+The template context is the same as [`body`](#body) and contains all the releases instead of a single release.
+
+For example, to get the list of releases, use the `{{ releases }}` variable in the template. To get information about a single release, iterate over this array and access the fields similar to [`body`](#body).
+
+See [Keep a Changelog configuration](/docs/templating/examples#keep-a-changelog) for seeing the example of adding links to the end of the changelog.
+
 ### trim
 
 If set to `true`, leading and trailing whitespace are removed from the [`body`](#body).
 
 It is useful for adding indentation to the template for readability, as shown [in the example](#changelog).
 
-### footer
-
-Footer text that will be added to the end of the changelog.
-
 ### postprocessors
 
 An array of commit postprocessors for manipulating the changelog before outputting.
diff --git a/website/docs/templating/context.md b/website/docs/templating/context.md
index 9ba2daa..e1620f8 100644
--- a/website/docs/templating/context.md
+++ b/website/docs/templating/context.md
@@ -1,6 +1,7 @@
 ---
 sidebar_position: 1
 ---
+
 # Context
 
 Context is the model that holds the required data for a template rendering. The [JSON](https://en.wikipedia.org/wiki/JSON) format is used in the following examples for the representation of a context.
diff --git a/website/docs/templating/examples.md b/website/docs/templating/examples.md
index 8a6e43a..a943813 100644
--- a/website/docs/templating/examples.md
+++ b/website/docs/templating/examples.md
@@ -662,6 +662,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 - Rename help argument due to conflict
 
+[unreleased]: https://github.com/orhun/git-cliff/compare/v1.0.1..HEAD
+[1.0.1]: https://github.com/orhun/git-cliff/compare/v1.0.0..v1.0.1
+
 <!-- generated by git-cliff -->
 ```
 
@@ -709,6 +712,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 - Rename help argument due to conflict
 
+[unreleased]: https://github.com/orhun/git-cliff/compare/v1.0.1..HEAD
+[1.0.1]: https://github.com/orhun/git-cliff/compare/v1.0.0..v1.0.1
+
 <!-- generated by git-cliff -->
 
 </details>
diff --git a/website/package.json b/website/package.json
index c9af593..a9a671a 100644
--- a/website/package.json
+++ b/website/package.json
@@ -1,6 +1,7 @@
 {
   "name": "git-cliff-website",
   "version": "0.0.0",
+  "license": "MIT OR Apache-2.0",
   "private": true,
   "scripts": {
     "docusaurus": "docusaurus",


Test patch:
diff --git a/.github/fixtures/test-bump-version/cliff.toml b/.github/fixtures/test-bump-version/cliff.toml
index b2a09b9..ec2fc39 100644
--- a/.github/fixtures/test-bump-version/cliff.toml
+++ b/.github/fixtures/test-bump-version/cliff.toml
@@ -19,9 +19,9 @@ body = """
     {% endfor %}
 {% endfor %}\n
 """
-# remove the leading and trailing whitespace from the template
-trim = true
-# changelog footer
+# template for the changelog footer
 footer = """
 <!-- generated by git-cliff -->
 """
+# remove the leading and trailing whitespace from the templates
+trim = true
diff --git a/.github/fixtures/test-bumped-version/cliff.toml b/.github/fixtures/test-bumped-version/cliff.toml
index b2a09b9..ec2fc39 100644
--- a/.github/fixtures/test-bumped-version/cliff.toml
+++ b/.github/fixtures/test-bumped-version/cliff.toml
@@ -19,9 +19,9 @@ body = """
     {% endfor %}
 {% endfor %}\n
 """
-# remove the leading and trailing whitespace from the template
-trim = true
-# changelog footer
+# template for the changelog footer
 footer = """
 <!-- generated by git-cliff -->
 """
+# remove the leading and trailing whitespace from the templates
+trim = true
diff --git a/.github/fixtures/test-commit-footers/cliff.toml b/.github/fixtures/test-commit-footers/cliff.toml
index adbecca..e143f3b 100644
--- a/.github/fixtures/test-commit-footers/cliff.toml
+++ b/.github/fixtures/test-commit-footers/cliff.toml
@@ -22,9 +22,9 @@ body = """
     {% endfor %}
 {% endfor %}\n
 """
-# remove the leading and trailing whitespace from the template
-trim = true
-# changelog footer
+# template for the changelog footer
 footer = """
 <!-- generated by git-cliff -->
 """
+# remove the leading and trailing whitespace from the templates
+trim = true
diff --git a/.github/fixtures/test-commit-preprocessors/cliff.toml b/.github/fixtures/test-commit-preprocessors/cliff.toml
index c6c7391..2d86f4c 100644
--- a/.github/fixtures/test-commit-preprocessors/cliff.toml
+++ b/.github/fixtures/test-commit-preprocessors/cliff.toml
@@ -19,12 +19,12 @@ body = """
     {% endfor %}
 {% endfor %}\n
 """
-# remove the leading and trailing whitespaces from the template
-trim = true
-# changelog footer
+# template for the changelog footer
 footer = """
 <!-- generated by git-cliff -->
 """
+# remove the leading and trailing whitespace from the templates
+trim = true
 
 [git]
 # regex for preprocessing the commit messages
diff --git a/.github/fixtures/test-custom-scope/cliff.toml b/.github/fixtures/test-custom-scope/cliff.toml
index 44a7161..fe30b90 100644
--- a/.github/fixtures/test-custom-scope/cliff.toml
+++ b/.github/fixtures/test-custom-scope/cliff.toml
@@ -22,12 +22,12 @@ body = """
     {% endfor %}\
 {% endfor %}\n
 """
-# remove the leading and trailing whitespaces from the template
-trim = true
-# changelog footer
+# template for the changelog footer
 footer = """
 <!-- generated by git-cliff -->
 """
+# remove the leading and trailing whitespace from the templates
+trim = true
 
 [git]
 # regex for parsing and grouping commits
diff --git a/.github/fixtures/test-date-order/cliff.toml b/.github/fixtures/test-date-order/cliff.toml
index 81b8495..5504810 100644
--- a/.github/fixtures/test-date-order/cliff.toml
+++ b/.github/fixtures/test-date-order/cliff.toml
@@ -19,9 +19,9 @@ body = """
     {% endfor %}
 {% endfor %}\n
 """
-# remove the leading and trailing whitespace from the template
-trim = true
-# changelog footer
+# template for the changelog footer
 footer = """
 <!-- generated by git-cliff -->
 """
+# remove the leading and trailing whitespace from the templates
+trim = true
diff --git a/.github/fixtures/test-footer-template/cliff.toml b/.github/fixtures/test-footer-template/cliff.toml
new file mode 100644
index 00000000..ebfa371
--- /dev/null
+++ b/.github/fixtures/test-footer-template/cliff.toml
@@ -0,0 +1,35 @@
+[changelog]
+# changelog header
+header = """
+# Changelog\n
+All notable changes to this project will be documented in this file.\n
+"""
+# template for the changelog body
+# https://keats.github.io/tera/docs/#introduction
+body = """
+{% if version %}\
+    ## [{{ version | trim_start_matches(pat="v") }}]
+{% else %}\
+    ## [unreleased]
+{% endif %}\
+{% for group, commits in commits | group_by(attribute="group") %}
+    ### {{ group | upper_first }}
+    {% for commit in commits %}
+        - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\
+    {% endfor %}
+{% endfor %}\n
+"""
+# template for the changelog footer
+footer = """
+{% for release in releases %}\
+    {% if release.version %}\
+        {% if release.previous.version %}\
+            <!--{{ release.previous.version }}..{{ release.version }}-->
+        {% endif %}\
+    {% else %}\
+        <!--{{ release.previous.version }}..HEAD-->
+    {% endif %}\
+{% endfor %}\
+"""
+# remove the leading and trailing whitespace from the templates
+trim = true
diff --git a/.github/fixtures/test-footer-template/commit.sh b/.github/fixtures/test-footer-template/commit.sh
new file mode 100755
index 00000000..ed35b19
--- /dev/null
+++ b/.github/fixtures/test-footer-template/commit.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+set -e
+
+GIT_COMMITTER_DATE="2021-01-23 01:23:45" git commit --allow-empty -m "feat: add feature 1"
+GIT_COMMITTER_DATE="2021-01-23 01:23:45" git commit --allow-empty -m "feat: add feature 2"
+git tag v0.1.0
+
+GIT_COMMITTER_DATE="2021-01-23 01:23:46" git commit --allow-empty -m "fix: fix feature 1"
+GIT_COMMITTER_DATE="2021-01-23 01:23:46" git commit --allow-empty -m "fix: fix feature 2"
+git tag v0.2.0
+
+GIT_COMMITTER_DATE="2021-01-23 01:23:47" git commit --allow-empty -m "feat: add footer"
+git tag v3.0.0
+
+GIT_COMMITTER_DATE="2021-01-23 01:23:47" git commit --allow-empty -m "test: footer"
+GIT_COMMITTER_DATE="2021-01-23 01:23:47" git commit --allow-empty -m "perf: footer"
diff --git a/.github/fixtures/test-footer-template/expected.md b/.github/fixtures/test-footer-template/expected.md
new file mode 100644
index 00000000..8ed45d5
--- /dev/null
+++ b/.github/fixtures/test-footer-template/expected.md
@@ -0,0 +1,38 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [unreleased]
+
+### Perf
+
+- Footer
+
+### Test
+
+- Footer
+
+## [3.0.0]
+
+### Feat
+
+- Add footer
+
+## [0.2.0]
+
+### Fix
+
+- Fix feature 1
+- Fix feature 2
+
+## [0.1.0]
+
+### Feat
+
+- Add feature 1
+- Add feature 2
+
+<!--v3.0.0..HEAD-->
+<!--v0.2.0..v3.0.0-->
+<!--v0.1.0..v0.2.0-->
+
diff --git a/.github/fixtures/test-ignore-tags/cliff.toml b/.github/fixtures/test-ignore-tags/cliff.toml
index 7d381cc..62a6561 100644
--- a/.github/fixtures/test-ignore-tags/cliff.toml
+++ b/.github/fixtures/test-ignore-tags/cliff.toml
@@ -19,12 +19,12 @@ body = """
     {% endfor %}
 {% endfor %}\n
 """
-# remove the leading and trailing whitespace from the template
-trim = true
-# changelog footer
+# template for the changelog footer
 footer = """
 <!-- generated by git-cliff -->
 """
+# remove the leading and trailing whitespace from the templates
+trim = true
 
 [git]
 # regex for skipping tags
diff --git a/.github/fixtures/test-keep-a-changelog-links/cliff.toml b/.github/fixtures/test-keep-a-changelog-links/cliff.toml
index 4e86ef7..c907a04 100644
--- a/.github/fixtures/test-keep-a-changelog-links/cliff.toml
+++ b/.github/fixtures/test-keep-a-changelog-links/cliff.toml
@@ -38,12 +38,12 @@ body = """
     {% endfor %}
 {% endfor %}\n
 """
-# remove the leading and trailing whitespace from the template
-trim = true
-# changelog footer
+# template for the changelog footer
 footer = """
 <!-- generated by git-cliff -->
 """
+# remove the leading and trailing whitespace from the templates
+trim = true
 
 [git]
 # parse the commits based on https://www.conventionalcommits.org
diff --git a/.github/fixtures/test-latest-with-one-tag/cliff.toml b/.github/fixtures/test-latest-with-one-tag/cliff.toml
index b8b70e5..5504810 100644
--- a/.github/fixtures/test-latest-with-one-tag/cliff.toml
+++ b/.github/fixtures/test-latest-with-one-tag/cliff.toml
@@ -19,9 +19,9 @@ body = """
     {% endfor %}
 {% endfor %}\n
 """
-# remove the leading and trailing whitespaces from the template
-trim = true
-# changelog footer
+# template for the changelog footer
 footer = """
 <!-- generated by git-cliff -->
 """
+# remove the leading and trailing whitespace from the templates
+trim = true
diff --git a/.github/fixtures/test-limit-commits/cliff.toml b/.github/fixtures/test-limit-commits/cliff.toml
index 859df10..f053d36 100644
--- a/.github/fixtures/test-limit-commits/cliff.toml
+++ b/.github/fixtures/test-limit-commits/cliff.toml
@@ -19,12 +19,12 @@ body = """
     {% endfor %}
 {% endfor %}\n
 """
-# remove the leading and trailing whitespace from the template
-trim = true
-# changelog footer
+# template for the changelog footer
 footer = """
 <!-- generated by git-cliff -->
 """
+# remove the leading and trailing whitespace from the templates
+trim = true
 
 [git]
 limit_commits = 2
diff --git a/.github/fixtures/test-skip-breaking-changes/cliff.toml b/.github/fixtures/test-skip-breaking-changes/cliff.toml
index c46d5ea..d057483 100644
--- a/.github/fixtures/test-skip-breaking-changes/cliff.toml
+++ b/.github/fixtures/test-skip-breaking-changes/cliff.toml
@@ -22,12 +22,12 @@ body = """
     {% endfor %}\
 {% endfor %}\n
 """
-# remove the leading and trailing whitespaces from the template
-trim = true
-# changelog footer
+# template for the changelog footer
 footer = """
 <!-- generated by git-cliff -->
 """
+# remove the leading and trailing whitespace from the templates
+trim = true
 
 [git]
 # regex for parsing and grouping commits
diff --git a/.github/fixtures/test-split-commits/cliff.toml b/.github/fixtures/test-split-commits/cliff.toml
index 83ed5d0..ec20df8 100644
--- a/.github/fixtures/test-split-commits/cliff.toml
+++ b/.github/fixtures/test-split-commits/cliff.toml
@@ -19,12 +19,12 @@ body = """
     {% endfor %}
 {% endfor %}\n
 """
-# remove the leading and trailing whitespaces from the template
-trim = true
-# changelog footer
+# template for the changelog footer
 footer = """
 <!-- generated by git-cliff -->
 """
+# remove the leading and trailing whitespace from the templates
+trim = true
 
 [git]
 # process each line of a commit as an individual commit
diff --git a/.github/fixtures/test-topo-order-arg/cliff.toml b/.github/fixtures/test-topo-order-arg/cliff.toml
index 81b8495..5504810 100644
--- a/.github/fixtures/test-topo-order-arg/cliff.toml
+++ b/.github/fixtures/test-topo-order-arg/cliff.toml
@@ -19,9 +19,9 @@ body = """
     {% endfor %}
 {% endfor %}\n
 """
-# remove the leading and trailing whitespace from the template
-trim = true
-# changelog footer
+# template for the changelog footer
 footer = """
 <!-- generated by git-cliff -->
 """
+# remove the leading and trailing whitespace from the templates
+trim = true
diff --git a/.github/fixtures/test-topo-order/cliff.toml b/.github/fixtures/test-topo-order/cliff.toml
index 2569eb7..f4bbb0a 100644
--- a/.github/fixtures/test-topo-order/cliff.toml
+++ b/.github/fixtures/test-topo-order/cliff.toml
@@ -19,12 +19,12 @@ body = """
     {% endfor %}
 {% endfor %}\n
 """
-# remove the leading and trailing whitespace from the template
-trim = true
-# changelog footer
+# template for the changelog footer
 footer = """
 <!-- generated by git-cliff -->
 """
+# remove the leading and trailing whitespace from the templates
+trim = true
 
 [git]
 topo_order = true
diff --git a/.github/workflows/test-fixtures.yml b/.github/workflows/test-fixtures.yml
index 8f9512a..7ae89bb 100644
--- a/.github/workflows/test-fixtures.yml
+++ b/.github/workflows/test-fixtures.yml
@@ -36,6 +36,7 @@ jobs:
             command: --bump
           - fixtures-name: test-bumped-version
             command: --bumped-version
+          - fixtures-name: test-footer-template
     steps:
       - name: Checkout
         uses: actions/checkout@v4
diff --git a/git-cliff-core/tests/integration_test.rs b/git-cliff-core/tests/integration_test.rs
index cc8c638..31096f6 100644
--- a/git-cliff-core/tests/integration_test.rs
+++ b/git-cliff-core/tests/integration_test.rs
@@ -22,7 +22,7 @@ fn generate_changelog() -> Result<()> {
 		header:         Some(String::from("this is a changelog")),
 		body:           Some(String::from(
 			r#"
-## Release {{ version }}
+## Release {{ version }} - <DATE>
 {% for group, commits in commits | group_by(attribute="group") %}
 ### {{ group }}
 {% for commit in commits %}
@@ -199,18 +199,27 @@ fn generate_changelog() -> Result<()> {
 	];
 
 	let out = &mut String::new();
-	let template = Template::new(changelog_config.body.unwrap())?;
+	let template = Template::new(changelog_config.body.unwrap(), false)?;
 
 	writeln!(out, "{}", changelog_config.header.unwrap()).unwrap();
 	for release in releases {
-		write!(out, "{}", template.render(&release)?).unwrap();
+		write!(
+			out,
+			"{}",
+			template.render(&release, &[TextProcessor {
+				pattern:         Regex::new("<DATE>").unwrap(),
+				replace:         Some(String::from("2023")),
+				replace_command: None,
+			}])?
+		)
+		.unwrap();
 	}
 	writeln!(out, "{}", changelog_config.footer.unwrap()).unwrap();
 
 	assert_eq!(
 		r#"this is a changelog
 
-## Release v2.0.0
+## Release v2.0.0 - 2023
 
 ### docs
 - *(cool)* testing author filtering
@@ -229,7 +238,7 @@ fn generate_changelog() -> Result<()> {
 ### test
 - *(tests)* test some stuff
 
-## Release v1.0.0
+## Release v1.0.0 - 2023
 
 ### chore
 - do nothing