`. That is a fix, but it changes vertical spacing.
⚠️ **If your CSS styles bare text nodes rather than ``, add a `p` rule
before the switch.** New paragraphs otherwise inherit browser default margins
and the page's rhythm shifts — a change noticed a week later and blamed
elsewhere.
---
# Part 2 — Enabling it on a site
For whoever operates a site on this framework. Part 1 is what you hand your
content authors; this is what you need before you flip the switch.
## Do I have it?
Introduced in **v1.6.11**. The check that does not go stale:
```
grep -c render_content_type1 main/xapserver.gf.php
```
Non-zero means your tree has it. Absent, and type 1 is still plain `nl2br`.
## Turning it on
One line in the site's `conf/xapserver.ext.class.php`:
```php
var $ENABLE_MARKDOWN_CONTENT = TRUE;
```
The property is declared `FALSE` in `main/xapserver.class.php:345`, so
**every site is off until it opts in.** Upgrading the framework changes no
site's rendering. `sample-domain/conf/xapserver.ext.class.php:47` carries the
line with its rationale, ready to be flipped.
**The name has no `PAGE_` prefix on purpose.** It states how the site authors
content, not which module renders it — so when XEdit gains markdown it will
read this same flag. Expect that: a site opted in here is opting in for
markdown wherever the framework renders authored text, not for page content
alone.
**The `?md=` query parameter overrides the flag in both directions**, on any
page, whether or not the site has opted in. `?md=1` forces markdown, `?md=0`
forces `nl2br`. That is deliberate: it lets you A/B a live page before
committing the site, and lets you rule markdown in or out when a page renders
oddly after you have. Anything other than those two values is ignored.
## Where it renders
One function, `render_content_type1()` (`main/xapserver.gf.php:2077`), holds
both the flag and the query override. Everything routes through it, so
**content type 1 means one thing across the framework**:
| caller | reaches it via |
|---|---|
| `PageTemplate::content()` case 1 | directly (`pagetemplate.class.php:108`, `:111`) |
| `Template::content()` case 1 | `echo_content_semiformatted()` (`xapserver.gf.php:383`) |
| any template calling `echo_content_semiformatted()` | same |
Both template base classes are covered. If your site defines its own
`content()`, it is not — route it through `render_content_type1()`.
`PageTemplate` additionally renders `page_localized_content.localized_content`
through the same function when `$ENABLE_LOCALIZED_CONTENT` is set and the
locale is not the default, so localized content and file content render
identically.
## The parser
Parsedown, vendored single-file at `include/Parsedown.php` (MIT), loaded
lazily on first use and reused for the request. Two configuration decisions
worth knowing, both deliberate:
- **Hard-breaks on** (`setBreaksEnabled(TRUE)`) — not standard CommonMark.
Standard Markdown folds consecutive lines into one paragraph; here a single
newline is a `
`. **This is what lets existing content survive the
switch** — under `nl2br` every newline was already a break, and turning
this off would silently reflow every page.
- **No safe mode, no markup escaping.** Raw HTML passes through untouched.
Type 1 was already echoed unescaped, and the same editor offers types 2
(raw HTML) and 3 (`eval`'d PHP) — so escaping here would protect nothing
while breaking every page that already contains a tag.
## Legacy content and invalid UTF-8
`markdown()` (`main/xapserver.gf.php:2092`) checks the text's encoding and
converts any non-UTF-8 **line** from Windows-1252 before parsing.
This is not defensive padding. Parsedown escapes with
`htmlspecialchars(…, 'UTF-8')` and **without `ENT_SUBSTITUTE`**, which returns
an empty string on invalid input — so a single stray byte silently deletes the
entire text node containing it. Content authored in Word carries CP1252 smart
quotes (`0x92`) and en-dashes (`0x96`) constantly; on this server three pages
lost whole sentences that way before the guard existed.
The conversion is **per line**, so one bad line cannot re-encode a good one.
## Adopting it on an existing site
Ordered. Steps 1–3 happen before anything renders differently.
1. **Diff the corpus first.** `bin/xs-markdown-diff.php` renders every type-1
file both ways and reports which differ and how. **Run it while the old
rendering still exists** — afterwards the "before" is gone and the
comparison cannot be reconstructed.
2. **Read the pages that lose `
` tags.** Those are the review set, and
the count is an upper bound rather than a work list: only content whose
*layout depended on* the dropped breaks needs a human.
3. **Check the CSS.** If any rule styles bare text nodes rather than `
`,
add the `p` rule now. New paragraphs otherwise inherit browser default
margins, and the rhythm shift gets noticed a week later and blamed
elsewhere.
4. **Normalise blank lines after block tags.** A `
` with prose on the
next line keeps that prose raw while later paragraphs on the same page
become `
` — a mid-page inconsistency, which is worse than a uniform
shift. Do this *with* the switch, not before: under `nl2br` the added
blank line is a visible extra break.
5. **Flip the flag.** Spot-check with `?md=0` to compare against the old
rendering on the same page.
## Notes
- **An unknown `content_type_id` renders nothing and says so.** Both switches
carry a `default:` branch that logs the id and path via `error_log()`.
Before that existed, an unrecognised type rendered a blank page silently —
which is the trap if you ever seed a new content type before its render
case ships. **Order is code first, data second.**
- **`content_type_id = 0` is not a content type.** It is `fid`'s default,
written by an insert that omitted the column, and it takes the `default:`
branch. There is deliberately **no `case 0`** — formalising an unset default
would encode an accident as a contract. If those log lines appear, they are
naming rows to fix, not noise to filter: three such rows survived on a
production site from 2006 to 2026 precisely because nothing said anything.
- Markdown is `case 1`, not a new content type. Nothing was added to the
`content_type` table, so no data migration is needed on any site.