diff --git a/docling_core/transforms/serializer/html.py b/docling_core/transforms/serializer/html.py
index 862ee184..0db93b47 100644
--- a/docling_core/transforms/serializer/html.py
+++ b/docling_core/transforms/serializer/html.py
@@ -443,6 +443,11 @@ def serialize(
text_res = "".join([r.text for r in res_parts])
text_res = f"
" if text_res else ""
+ ftn_res = doc_serializer.serialize_footnotes(item=item, **kwargs)
+ if ftn_res.text:
+ text_res = f"{text_res}{ftn_res.text}" if text_res else ftn_res.text
+ res_parts.append(ftn_res)
+
return create_ser_result(text=text_res, span_source=res_parts)
@override
@@ -1276,6 +1281,33 @@ def serialize_captions(
text_res = f"<{tag}>{text_res}{tag}>"
return create_ser_result(text=text_res, span_source=results)
+ @override
+ def serialize_footnotes(
+ self,
+ item: FloatingItem,
+ **kwargs: Any,
+ ) -> SerializationResult:
+ """Serialize the item's footnotes."""
+ params = self.params.merge_with_patch(patch=kwargs)
+ results: list[SerializationResult] = []
+ excluded_refs = self.get_excluded_refs(**kwargs)
+
+ if DocItemLabel.FOOTNOTE in params.labels:
+ for ftn in item.footnotes:
+ if isinstance(it := ftn.resolve(self.doc), TextItem) and it.self_ref not in excluded_refs:
+ text_ftn = it.text
+ text_dir = get_text_direction(text_ftn)
+ dir_str = f' dir="{text_dir}"' if text_dir == "rtl" else ""
+ results.append(
+ create_ser_result(
+ text=(f''),
+ span_source=it,
+ )
+ )
+
+ text_res = params.caption_delim.join([r.text for r in results])
+ return create_ser_result(text=text_res, span_source=results)
+
def _generate_head(self) -> str:
"""Generate the HTML head section with metadata and styles."""
params = self.params
diff --git a/docling_core/transforms/serializer/markdown.py b/docling_core/transforms/serializer/markdown.py
index 066788d2..e852e6c2 100644
--- a/docling_core/transforms/serializer/markdown.py
+++ b/docling_core/transforms/serializer/markdown.py
@@ -576,6 +576,10 @@ def serialize(
if table_text:
res_parts.append(create_ser_result(text=table_text, span_source=item))
+ ftn_res = doc_serializer.serialize_footnotes(item=item, **kwargs)
+ if ftn_res.text:
+ res_parts.append(ftn_res)
+
text_res = "\n\n".join([r.text for r in res_parts])
return create_ser_result(text=text_res, span_source=res_parts)
diff --git a/test/test_serialization.py b/test/test_serialization.py
index b6bd4b1d..0ed17403 100644
--- a/test/test_serialization.py
+++ b/test/test_serialization.py
@@ -1103,3 +1103,36 @@ def test_html_meta_emits_xhtml_compatible_attributes():
assert 'data-meta-name="entities"' in html_out
# Output must be parseable by a strict XML parser.
ET.fromstring(html_out)
+
+
+def test_table_footnotes_serialized_to_markdown_and_html():
+ """Regression test for #496: table footnotes must not be dropped by the
+ Markdown and HTML serializers (they are delegated to the parent TableItem)."""
+ doc = DoclingDocument(name="t")
+
+ td = TableData(num_rows=0, num_cols=2)
+ td.add_row(["Header 1", "Header 2"])
+ td.add_row(["Data 1", "Data 2"])
+ table = doc.add_table(data=td)
+
+ fn1 = doc.add_text(label=DocItemLabel.FOOTNOTE, text="First footnote.")
+ fn2 = doc.add_text(label=DocItemLabel.FOOTNOTE, text="Second footnote.")
+ table.footnotes.append(fn1.get_ref())
+ table.footnotes.append(fn2.get_ref())
+
+ # footnotes are stored on the table in the document model ...
+ assert [fn.resolve(doc).text for fn in table.footnotes] == [
+ "First footnote.",
+ "Second footnote.",
+ ]
+
+ # ... and must survive Markdown serialization
+ md_out = doc.export_to_markdown()
+ assert "First footnote." in md_out
+ assert "Second footnote." in md_out
+
+ # ... and HTML serialization (as escaped footnote divs after the table)
+ html_out = doc.export_to_html()
+ assert "First footnote." in html_out
+ assert "Second footnote." in html_out
+ assert '' in html_out