diff --git a/src/MfGames.Nitride.Markdown/ParseMarkdownHeadingOne.cs b/src/MfGames.Nitride.Markdown/ParseMarkdownHeadingOne.cs
new file mode 100644
index 0000000..73d6a14
--- /dev/null
+++ b/src/MfGames.Nitride.Markdown/ParseMarkdownHeadingOne.cs
@@ -0,0 +1,88 @@
+using FluentValidation;
+using Markdig.Renderers.Roundtrip;
+using Markdig.Syntax;
+using MfGames.Gallium;
+using MfGames.Nitride.Contents;
+using MfGames.Nitride.Generators;
+
+namespace MfGames.Nitride.Markdown;
+
+///
+/// An operation that parses the Markdown and converts the first heading one
+/// into a model to include in the component. The rest of the Markdown is put
+/// back as the text content of the entity.
+///
+[WithProperties]
+public partial class ParseMarkdownHeadingOne : IOperation
+{
+ private readonly IValidator validator;
+
+ public ParseMarkdownHeadingOne(IValidator validator)
+ {
+ this.validator = validator;
+ }
+
+ ///
+ /// Gets or sets a callback for adding a heading to a given entity.
+ ///
+ public Func? AddModelCallback { get; set; }
+
+ ///
+ public IEnumerable Run(
+ IEnumerable input,
+ CancellationToken cancellationToken = default
+ )
+ {
+ this.validator.ValidateAndThrow(this);
+
+ return input.SelectManyEntity(x => x.Select(this.Parse));
+ }
+
+ private string? GetText(MarkdownObject? block)
+ {
+ if (block == null)
+ return null;
+
+ var writer = new StringWriter();
+ var renderer = new RoundtripRenderer(writer);
+
+ renderer.Write(block);
+
+ return writer.ToString();
+ }
+
+ private Entity Parse(Entity entity)
+ {
+ // Get the text content of the file. No text, we don't do anything (but
+ // there is going to be text since we filtered on IsMarkdown).
+ string? oldText = entity.GetTextContentString();
+
+ if (oldText == null)
+ {
+ return entity;
+ }
+
+ // Parse the result as Markdown and pull out the heading. If we can't
+ // find one, then we just return the entity. We need to track trivia
+ // because we are round-tripping back to Markdown.
+ MarkdownDocument document = Markdig.Markdown.Parse(oldText, true);
+ Block? block = document.FirstOrDefault(block => block is HeadingBlock);
+
+ if (block is not HeadingBlock { Level: 1 } heading)
+ {
+ return entity;
+ }
+
+ string? headingText = this.GetText(heading.Inline);
+
+ // Convert the heading into the model.
+ // Pull out the heading so we can write the rest back.
+ document.Remove(heading);
+
+ string newText = this.GetText(document)!;
+
+ // Allow the extending class to add the model to the entity and then
+ // set the text content to the new value before returning the results.
+ return this.AddModelCallback!.Invoke(entity, headingText).SetTextContent(newText);
+ }
+}
diff --git a/src/MfGames.Nitride.Markdown/Validators/ParseMarkdownHeadingOneValidator.cs b/src/MfGames.Nitride.Markdown/Validators/ParseMarkdownHeadingOneValidator.cs
new file mode 100644
index 0000000..73c8aad
--- /dev/null
+++ b/src/MfGames.Nitride.Markdown/Validators/ParseMarkdownHeadingOneValidator.cs
@@ -0,0 +1,11 @@
+using FluentValidation;
+
+namespace MfGames.Nitride.Markdown.Validators;
+
+public class ParseMarkdownHeadingOneValidator : AbstractValidator
+{
+ public ParseMarkdownHeadingOneValidator()
+ {
+ this.RuleFor(x => x.AddModelCallback).NotNull();
+ }
+}
diff --git a/tests/MfGames.Nitride.Markdown.Tests/ParseMarkdownHeadingOneTests.cs b/tests/MfGames.Nitride.Markdown.Tests/ParseMarkdownHeadingOneTests.cs
new file mode 100644
index 0000000..1dbd695
--- /dev/null
+++ b/tests/MfGames.Nitride.Markdown.Tests/ParseMarkdownHeadingOneTests.cs
@@ -0,0 +1,154 @@
+using System.Collections.Generic;
+using System.Linq;
+using MfGames.Gallium;
+using MfGames.Nitride.Contents;
+using MfGames.TestSetup;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace MfGames.Nitride.Markdown.Tests;
+
+///
+/// Tests the functionality of the ParseMarkdownHeadingOne operation.
+///
+public class ParseMarkdownHeadingOneTests : TestBase
+{
+ public ParseMarkdownHeadingOneTests(ITestOutputHelper output)
+ : base(output) { }
+
+ [Fact]
+ public void ParseComplexHeader()
+ {
+ using MarkdownTestContext context = this.CreateContext();
+
+ List input =
+ new()
+ {
+ new Entity()
+ .Set(IsMarkdown.Instance)
+ .SetTextContent("# Heading [One](/test)\n\nContent\nsecond\n\nline"),
+ };
+
+ ParseMarkdownHeadingOne op = context
+ .Resolve()
+ .WithAddModelCallback((entity, heading) => entity.Set(heading));
+
+ IEnumerable output = op.Run(input);
+ Entity first = output.First();
+ string content = first.GetTextContentString()!.Trim();
+
+ Assert.Equal("Content\nsecond\n\nline", content);
+ Assert.Equal("Heading [One](/test)", first.Get());
+ }
+
+ [Fact]
+ public void ParseEmptyHeader()
+ {
+ using MarkdownTestContext context = this.CreateContext();
+
+ List input =
+ new() { new Entity().Set(IsMarkdown.Instance).SetTextContent("#\nContent"), };
+
+ ParseMarkdownHeadingOne op = context
+ .Resolve()
+ .WithAddModelCallback((entity, heading) => entity.Set(heading));
+
+ IEnumerable output = op.Run(input);
+ Entity first = output.First();
+ string content = first.GetTextContentString()!.Trim();
+
+ Assert.Equal("Content", content);
+ Assert.Equal("", first.Get());
+ }
+
+ [Fact]
+ public void ParseHeaderTwo()
+ {
+ using MarkdownTestContext context = this.CreateContext();
+
+ List input =
+ new()
+ {
+ new Entity().Set(IsMarkdown.Instance).SetTextContent("## Heading Two\nContent"),
+ };
+
+ ParseMarkdownHeadingOne op = context
+ .Resolve()
+ .WithAddModelCallback((entity, heading) => entity.Set(heading));
+
+ IEnumerable output = op.Run(input);
+ Entity first = output.First();
+ string content = first.GetTextContentString()!.Trim();
+
+ Assert.Equal("## Heading Two\nContent", content);
+ Assert.False(first.Has());
+ }
+
+ [Fact]
+ public void ParseMultipleHeaderOne()
+ {
+ using MarkdownTestContext context = this.CreateContext();
+
+ List input =
+ new()
+ {
+ new Entity()
+ .Set(IsMarkdown.Instance)
+ .SetTextContent("# Heading One\n\n# Heading Two\n Content"),
+ };
+
+ ParseMarkdownHeadingOne op = context
+ .Resolve()
+ .WithAddModelCallback((entity, heading) => entity.Set(heading));
+
+ IEnumerable output = op.Run(input);
+ Entity first = output.First();
+ string content = first.GetTextContentString()!.Trim();
+
+ Assert.Equal("# Heading Two\n Content", content);
+ Assert.Equal("Heading One", first.Get());
+ }
+
+ [Fact]
+ public void ParseNoHeader()
+ {
+ using MarkdownTestContext context = this.CreateContext();
+
+ List input =
+ new() { new Entity().Set(IsMarkdown.Instance).SetTextContent("Content"), };
+
+ ParseMarkdownHeadingOne op = context
+ .Resolve()
+ .WithAddModelCallback((entity, heading) => entity.Set(heading));
+
+ IEnumerable output = op.Run(input);
+ Entity first = output.First();
+ string content = first.GetTextContentString()!.Trim();
+
+ Assert.Equal("Content", content);
+ Assert.False(first.Has());
+ }
+
+ [Fact]
+ public void ParseSimpleHeader()
+ {
+ using MarkdownTestContext context = this.CreateContext();
+
+ List input =
+ new()
+ {
+ new Entity().Set(IsMarkdown.Instance).SetTextContent("# Heading One\n\nContent"),
+ };
+
+ ParseMarkdownHeadingOne op = context
+ .Resolve()
+ .WithAddModelCallback((entity, heading) => entity.Set(heading));
+
+ IEnumerable output = op.Run(input);
+ Entity first = output.First();
+ string content = first.GetTextContentString()!.Trim();
+
+ Assert.Equal("Content", content);
+ Assert.Equal("Heading One", first.Get());
+ }
+}