Site Structure & Pages as Code: What Git Can (and Can't) Own

Lesson 9: Site Structure & Pages as Code โ€” What Git Can (and Can't) Own

Lesson 8 shipped your components. This lesson is about shipping the site itself: lists, navigation, and the modern pages people land on. The good news: site structure is beautifully scriptable from a repo. The honest news, up front: full page content round-tripping to git is not a real thing โ€” and the community consensus is that it shouldn't be.

The one-paragraph consensus (from PnP community and Microsoft guidance): structure = code โœ… โ€” lists, fields, content types, navigation, and branding belong in your repo as PnP templates or site scripts. page content = not code โŒ โ€” modern page text and web parts are stored as opaque client-side JSON inside .aspx files, are edited constantly by content editors, and break on git round-trip. The bridge between the two is scripted page provisioning: you keep page recipes in the repo, a pipeline builds the pages, and editors take over from there.

Why page content won't round-trip

Every modern page lives in the SitePages library as an .aspx file, but the actual content (text, web part instances, layout) is embedded as a serialized client-side data model โ€” not human-editable markup. SharePoint has no export API for it as portable content, and the schema shifts as Microsoft updates web parts. Export โ†’ edit โ†’ import reliably corrupts or loses things. So nobody sane stores page content in git; they store the instructions to create it.

Mechanism 1: PnP Provisioning Templates (structure)

A PnP template is a portable XML/JSON description of a site that the provisioning engine can apply anywhere. Capture it from a well-built "golden" site, commit it, and replay it on every new site โ€” in a pipeline, at scale.

Captured by templatesNot captured
Lists & libraries (schema, views, content types), fields with validation, navigation, files bundled as assets, site settings, branding, page metadata (name, layout, publish state)Modern page content โ€” web part configuration and text. Those must be scripted separately (Mechanism 3).
# Extract a template from a golden site (PnP PowerShell)
Get-PnPSiteTemplate -Out "./provisioning/team-site.xml" `
  -Handlers Lists,Fields,ContentTypes,Navigation,Files,SiteSettings,Branding

# Apply it to a new site
Invoke-PnPSiteTemplate -Path "./provisioning/team-site.xml" -ClearNavigation
Note for CLI users: unlike almost everything else in this course, applying PnP templates is a PnP PowerShell / .NET provisioning-engine capability โ€” CLI for Microsoft 365 currently has no site template command (older blog posts claiming one are stale). Run template apply steps with pwsh + PnP.PowerShell in your pipeline, e.g. through the PnP PowerShell OIDC action from Lesson 8.

Mechanism 2: Site scripts + site designs (lightweight, Microsoft-native)

Site scripts are JSON "verb recipes" (create a list, add a field, set the theme, add navigation) registered with the tenant and bundled into a site design that appears in the SharePoint "Create site" UI. They're perfect for lightweight, repeatable provisioning with zero PowerShell, and they can't touch page content either โ€” configuration only.

// site-scripts/project-site.json
{
  "$schema": "schema.json",
  "actions": [
    { "verb": "createSPList", "listName": "Projects", "templateType": 100 },
    { "verb": "addSPField", "listName": "Projects", "fieldType": "Text", "displayName": "Owner" },
    { "verb": "setSiteTheme", "themeName": "Contoso" }
  ]
}
# Register and wire it up with CLI for Microsoft 365
m365 spo sitescript add --title "Project site" --content "./site-scripts/project-site.json"
m365 spo sitedesign add --title "Project design" --webTemplate 64 `
  --siteScripts "<script-guid-from-previous-command>"

Choosing: site scripts win when the goal is "anyone can create a consistent site from the UI" and the scope is small. PnP templates win when you need real depth (views, files, complex content types, repeat application in CI/CD). Many shops use both: a site design for creation, then a template apply for the full structure.

Mechanism 3: Page recipes โ€” scripted page provisioning

For pages that must exist with a known structure (home page, department landing pages, documentation hubs), the accepted pattern is a page recipe checked into the repo: a small script that creates the page, adds text and web parts into defined sections, and publishes it.

# pages/home.ps1 โ€” a "recipe" your pipeline runs
$page = Add-PnPPage -Name "Home" -LayoutType Article
Add-PnPPageTextPart -Page $page -Section 1 -Column 1 `
  -Text "<h1>Welcome to Engineering</h1><p>Start here for team resources.</p>"
Add-PnPPageWebPart -Page $page -Section 1 -Column 2 `
  -ComponentId "490d5c76-18ea-4d9d-812a-254b51748115" `   # text web part
  -Properties @{ Title = "Announcements" }
Set-PnPPage -Identity $page -Published $true

The CLI equivalent covers the same ground: m365 spo page add --name Home.aspx --layoutType Article, then m365 spo page text add, m365 spo page clientsidewebpart add, and m365 spo page publish --id โ€ฆ (plus page list, page set, page copy for management).

Guard your idempotence: page recipes should be safe to re-run. Check existence first (Get-PnPClientSidePage -Identity Home / m365 spo page list), skip or update deliberately, and never let a pipeline blindly recreate a page that editors may have customized since provisioning. A common rule: recipe owns the page until the first human edit โ€” then SharePoint owns it.
The full picture now: SPFx packages (code) + PnP templates & site scripts (structure) + page recipes (known pages) all ride the same repo โ†’ Actions โ†’ tenant pipeline from Lesson 8. What stays in SharePoint: the living content editors produce every day. That split is the industry pattern, not a compromise.

๐Ÿง  Knowledge Check

1. What does a PnP Provisioning Template reliably capture from a site?

2. Why isn't modern page content stored in git as a rule?

3. What is a "page recipe" in the scripted-provisioning pattern?

Further Reading