Abdulkader Safi

The sitemap was not broken, it just looked it

A sitemap opened in a browser as a wall of unlabelled URLs and timestamps in a serif font. The XML was valid, correctly typed and parsed cleanly. What was missing is the thing every serious sitemap ships and mine did not, and the fix is one processing instruction crawlers ignore entirely.

1 min read

Share
A sitemap rendered in a browser as a styled table of URLs, last modified dates and locale badges.

v0.1.6 fixes one thing, and the interesting part is that nothing was actually wrong.

The report was that /sitemap.xml looked broken. What came back when opened in a browser was a single line of URLs and timestamps run together, in a serif font, with no structure at all:

http://localhost:8000/home 2026-08-18T14:02:07+00:00 http://localhost:8000/ar/home …

That is the classic shape of XML being parsed as HTML: unknown tags render as nothing, text nodes survive, and you get the content with all the markup dropped.

Proving it before fixing it

The temptation is to start changing headers. I checked the response first, because the fix for "browser renders it oddly" and the fix for "the file is malformed" are completely different.

Content-Type: application/xml; charset=UTF-8
00000000: 3c3f 786d 6c20 7665 7273 696f 6e3d 2231  <?xml version="1

Correct type, first bytes are the XML declaration, no byte order mark, and simplexml_load_string parsed it in the test suite. The file was fine.

There was also a second claim in the same report: a draft page appearing in the sitemap. That one I could check directly, and the timestamps told the story. The Contact page had published_at set several minutes after my seed ran, so it had been published in the panel while the site was open. The sitemap was right to list it.

Both halves of a bug report can be worth checking separately, and one of them being real does not make the other one real.

What was actually missing

An <?xml-stylesheet?> instruction. Every sitemap you have seen rendered as a neat table has one, and mine did not.

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="https://example.com/sitemap.xsl"?>
<urlset …>

Browsers apply the stylesheet and render a table of URLs, last-modified dates and locale alternates. Crawlers ignore the instruction entirely, so nothing about the machine-readable side changes. It is presentation for humans bolted onto a file written for robots, which is a slightly odd idea until you remember that the person opening it is usually the developer who just built it.

The stylesheet is served from the package rather than published into the host app's public directory, so it cannot drift from the XML it renders and there is nothing extra to publish or forget.

Verifying the fix, not the string

The lazy test here asserts that the output contains xml-stylesheet. That proves a line exists, not that the stylesheet works.

So I ran the actual transform the way a browser does:

xsltproc sitemap.xsl sitemap.xml

Which produced 4218 bytes of HTML with a table, six URLs, the right dates, and working per-locale links. That is the difference between testing that you wrote something and testing that it does something.

The test in the suite asserts both: that the instruction sits between the declaration and the root element, which is the only place it is allowed, and that the XML still parses with it there.

The other thing this cost

While chasing it I also spent a while on a page rendering with no CSS at all, in both the public site and the editor preview.

Nothing in the code. A stale public/hot file was sitting there pointing at a Vite dev server that was not running, so every page linked its stylesheet to a dead port. Vite writes that file on start and removes it on a clean exit, so killing the dev server hard leaves it behind and every page silently loses its styling until you delete it.

It was already the last entry in my troubleshooting docs, filed under "still stuck". It has since been promoted, because the symptom as experienced is "everything is unstyled" and nobody looks in a section called still stuck first.

Where it stands

v0.1.6. One processing instruction, one stylesheet served from the package, and a sitemap that reads as a table when a person opens it.

No migration, no API change. The kind of release that exists because a thing being correct and a thing looking correct are not the same, and only one of them gets reported.

Last updated 18 Aug 2026 · filed under Laravel, filamentphp, plugin, tools, web application, ux, seo

Every entry on this project

28 build notes, in order

Including the ones where nothing worked. You are on part 10.

Read the series →

FAQ

Frequently asked questions

Why does a sitemap render as a wall of text in a browser?

Because without a stylesheet the browser has nothing to lay the document out with, and depending on how it treats the response you either get a raw node tree or the text content with every tag dropped, which reads as one long run-on line. The file itself is usually fine. Adding an xml-stylesheet processing instruction between the XML declaration and the root element points at an XSL file that transforms the document into HTML for display. Browsers apply it, crawlers ignore it completely, and the machine-readable content is unchanged, so it is presentation added for the humans who open the file.

How do you check whether a sitemap is genuinely malformed or just rendering oddly?

Look at the response rather than the browser. Check the Content-Type header is an XML type, check the first bytes are the XML declaration with no byte order mark in front of it, and run the body through a parser. If the type is right, there is no leading whitespace or BOM, and it parses, the file is valid and the problem is presentation. Those three checks take a minute and they separate two problems whose fixes have nothing in common, which stops you changing headers to solve a styling issue.

How should you test that a stylesheet attached to XML actually works?

Run the transform. Asserting that the output contains the stylesheet reference only proves a line was written, not that the transform produces anything useful, and a broken XSL file will still be referenced correctly. Running the stylesheet against the document with a command line processor and checking the resulting HTML contains the expected table, rows and links tests the thing you actually shipped. Keep a cheaper assertion alongside it in the test suite for the structural rule, that the instruction sits between the declaration and the root element, since that is the only position where it is valid.

Why would every page on a Laravel site suddenly lose its CSS?

A leftover hot file. Vite writes a file into the public directory when the dev server starts, and the framework's asset helper checks for it: if present, it links assets to the dev server instead of the built files. The file is removed on a clean exit, so killing the dev server abruptly leaves it behind. Every page then links its stylesheet to a port with nothing listening, and the result is a completely unstyled site with no error anywhere, in the front end and in any admin preview alike. Deleting the file restores the built assets immediately.

Written by

Abdulkader Safi

Software Engineer

Lead engineer at DSRPT, from Lebanon and based in Kuwait. I write about the tools and bugs from real client work, with the numbers I measured.

About me → GitHub LinkedIn

Need this kind of work done on your project?

Start a project →

Keep reading

More from Filament Atelier

All 28 entries →
  • A version number changing from 0.5.0 to 1.0.0 beside a list of deferred items.
    Filament Atelier

    · part 28 of 28

    Tagging 1.0.0 with four features missing, on purpose

    The gate list had thirteen items. Six went in, seven did not, and the tag went out anyway. What the number promises is that the API stops moving, not that the feature list is finished, and conflating those two is how packages sit at 0.x for three years.

  • The same block of JavaScript appearing in three different Blade layout files.
    Filament Atelier

    · part 26 of 28

    A script in three layouts is a contract nobody signed

    The editor's preview needed a few lines of JavaScript in the page it renders. I put them in the shipped layout, then copied them into two more. Anyone writing their own layout had to copy them too, and missing them broke half the editor with no error at all.