10 KiB
Implementation Overview
This document describes the architecture and implementation plan for converting the Squarespace/WordPress XML export into a Next.js static site.
See EXPORT_XML_STRUCTURE.md for a detailed description of the XML format.
Project layout
.
├── content/
│ ├── posts/
│ │ ├── nyheter/ # Main news posts (33 files)
│ │ ├── historierogmyter/ # Stories & myths ( 6 files)
│ │ └── den-hviide-knap/ # DHK-specific news ( 9 files)
│ ├── pages/
│ │ ├── den-gyldne-gris-orden/ # DGG section (7 files, index.md = anno 1859)
│ │ ├── den-hviide-knap/ # DHK section (5 files)
│ │ ├── bokuglen/ # Bokuglen section (5 files, index.md = anno 1924)
│ │ ├── grisefestene/ # Festival pages (4 files, index.md)
│ │ ├── _squarespace/ # Squarespace template pages – not for rendering
│ │ ├── about.md
│ │ ├── chateau-neuf.md
│ │ └── contact.md
│ └── image-manifest.json # CDN URL → local path mapping for all 394 images
├── public/
│ └── images/ # Downloaded images
├── scripts/
│ ├── convert-xml.mjs # Step 1: XML → Markdown
│ ├── reorganize-content.mjs # Step 2: sort into section subdirectories (run once)
│ └── download-images.mjs # Step 3: fetch images from CDN
├── src/ # Next.js app source
└── export-2026-03-09.xml # Original Squarespace XML export
Site navigation (from hmgrisen.com)
The live site's top-level navigation, which determines the section structure above:
| Menu item | URL(s) | Content type |
|---|---|---|
| Nyheter | / |
Posts in posts/nyheter/ |
| Historier og myter | /historierogmyter |
Posts in posts/historierogmyter/ |
| Om | /about |
Static page |
| Den Gyldne Gris' Orden | /den-gyldne-gris-orden |
Section index + sub-pages |
| ↳ Anno 1859 | /den-gyldne-gris-orden |
pages/den-gyldne-gris-orden/index.md |
| ↳ Presensliste | /presensliste |
pages/den-gyldne-gris-orden/presensliste.md |
| ↳ Statutter | /statutter |
pages/den-gyldne-gris-orden/statutter.md |
| ↳ Embedsverk | /hoff |
pages/den-gyldne-gris-orden/hoff.md |
| ↳ Matrikkel | /matrikkel |
pages/den-gyldne-gris-orden/matrikkel.md |
| Den Hviide Knap | /anno-1899 |
Posts + static sub-pages |
| ↳ Knappenytt (news) | /anno-1899 |
Posts in posts/den-hviide-knap/ |
| ↳ Historie | /dhkhistorie |
pages/den-hviide-knap/dhkhistorie.md |
| ↳ Presensliste | /dhkpresensliste |
pages/den-hviide-knap/dhkpresensliste.md |
| ↳ Matrikkel | /dhkmatrikkel |
pages/den-hviide-knap/dhkmatrikkel.md |
| ↳ Statutter | /dhkstatutter |
pages/den-hviide-knap/dhkstatutter.md |
| ↳ Studentteateret 175 år | /studentteateret-175-r |
pages/den-hviide-knap/studentteateret-175-r.md |
| Bokuglen | /anno-1924 |
Section index + sub-pages |
| ↳ Anno 1924 | /anno-1924 |
pages/bokuglen/index.md |
| ↳ Bokuglens historie | /bokuglens-historie |
pages/bokuglen/bokuglens-historie.md |
| ↳ Presensliste | /bupresensliste |
pages/bokuglen/bupresensliste.md |
| ↳ Matrikkel | /bumatrikkel |
pages/bokuglen/bumatrikkel.md |
| ↳ Statutter | /bustatutter |
pages/bokuglen/bustatutter.md |
| Grisefestene | /grisefestene |
Section index + per-festival pages |
| Chateau Neuf | /chateau-neuf |
Static page |
| Det Norske Studentersamfund | studentersamfundet.no |
External link |
Step 1: XML → Markdown conversion (scripts/convert-xml.mjs)
Note: Steps 1–3 are one-time setup scripts, not part of the build pipeline.
Run:
node scripts/convert-xml.mjs # convert only
node scripts/convert-xml.mjs --download-images # convert + fetch all images
What it does
- Parses XML using
fast-xml-parser(handles CDATA, namespaces). - Builds an author map from
<wp:author>entries:email → display name. - Processes each
postandpageitem:- Resolves the author display name via the author map.
- Pre-processes WordPress
[caption]shortcodes → plain<img>with alt text. - Rewrites Squarespace CDN image URLs to local
/images/FILENAMEpaths. - Converts HTML → Markdown using
turndown. - Writes a
.mdfile with YAML frontmatter.
- Skips
attachmentitems (images are embedded in content and handled inline). - Writes
content/image-manifest.jsonmapping every unique CDN URL to its local filename.
Frontmatter fields
| Field | Source | Notes |
|---|---|---|
title |
<title> |
|
date |
<wp:post_date> |
ISO date, truncated to YYYY-MM-DD |
author |
<dc:creator> → author map |
Display name resolved from email |
status |
<wp:status> |
publish or draft |
type |
<wp:post_type> |
post or page |
oldPermalink |
<link> |
Original relative URL — used for redirect rules |
postName |
<wp:post_name> |
Original WP slug (may include path) |
excerpt |
<excerpt:encoded> |
Only present when non-empty |
Output filename conventions
- Posts:
content/posts/YYYY-MM-DD-<last-slug-segment>.md - Pages:
content/pages/<slug>.md
The last path segment of <wp:post_name> is used as the slug.
Image handling
- All
<img src="...">URLs in content are collected and deduplicated (by URL without query string). - Each gets a local filename derived from the last path segment of the CDN URL.
- The HTML is rewritten with local paths before HTML→Markdown conversion, so the Markdown references
/images/FILENAME. content/image-manifest.jsonmaps CDN URL →{ localPath, filename, downloadUrl }.- Actual file downloads happen only with
--download-images(supports one redirect hop).
Step 2: Content reorganization (scripts/reorganize-content.mjs)
Moves Markdown files into section subdirectories and adds a section frontmatter field.
Run once, after convert-xml.mjs. The section assignment uses the first path segment of
oldPermalink for posts, and exact oldPermalink matching for pages.
Step 3: Old permalink redirects
The oldPermalink frontmatter field contains the original Squarespace URL path (e.g. /nyheter/2013/10/6/nye-riddere-kommandrer-og-storkors). This enables a redirect map to be generated if the new URL structure differs.
Planned: A script or Next.js next.config.ts redirect generator that reads all Markdown frontmatter and emits redirects() entries mapping oldPermalink → newPath.
Step 3: Next.js rendering
Tech stack
| Layer | Choice |
|---|---|
| Framework | Next.js 16 App Router, static export (SSG) |
| Markdown parsing | gray-matter (frontmatter) + remark + remark-html |
| Styling | SASS — src/css/main.scss (no Tailwind) |
| Fonts | next/font/google — Cormorant Garamond + Cormorant SC |
Source layout
src/
app/ # Next.js App Router routes
layout.tsx # Root layout: header with logo + nav, imports main.scss
page.tsx # / → Nyheter index
nyheter/[slug]/page.tsx
historierogmyter/
page.tsx # section index
[slug]/page.tsx
den-gyldne-gris-orden/
page.tsx # index.md
[slug]/page.tsx # presensliste, statutter, hoff, matrikkel, …
anno-1899/
page.tsx # DHK post list
[slug]/page.tsx # DHK posts + static pages (dhkhistorie etc.)
anno-1924/
page.tsx # index.md (Bokuglen)
[slug]/page.tsx
grisefestene/
page.tsx # index.md
[slug]/page.tsx
about/page.tsx
chateau-neuf/page.tsx
components/
Nav.tsx # Site-wide nav with CSS hover dropdowns
SectionNav.tsx # Sub-section tab nav (takes links prop)
PageLayout.tsx # Wraps page: optional SectionNav + title + article body
css/
main.scss # All styles; variables at top
lib/
content.ts # Shared FS helpers: getPostsBySection, getPostBySlug,
# getSectionPage, getRootPage, getPostSlugs, getPageSlugs
URL → content mapping
| URL | Source file |
|---|---|
/ |
posts/nyheter/ index |
/nyheter/:slug |
posts/nyheter/YYYY-MM-DD-{slug}.md |
/historierogmyter/:slug |
posts/historierogmyter/YYYY-MM-DD-{slug}.md |
/den-gyldne-gris-orden |
pages/den-gyldne-gris-orden/index.md |
/den-gyldne-gris-orden/:slug |
pages/den-gyldne-gris-orden/{slug}.md |
/anno-1899 |
post list from posts/den-hviide-knap/ |
/anno-1899/:slug |
posts/den-hviide-knap/ OR pages/den-hviide-knap/ |
/anno-1924 |
pages/bokuglen/index.md |
/anno-1924/:slug |
pages/bokuglen/{slug}.md |
/grisefestene |
pages/grisefestene/index.md |
/grisefestene/:slug |
pages/grisefestene/{slug}.md |
/about |
pages/about.md |
/chateau-neuf |
pages/chateau-neuf.md |
Old Squarespace flat URLs are redirected in next.config.ts (redirects()).
Header
Logo image: public/images/hm-grisen-sign.png (downloaded from Squarespace CDN).
Tagline: the full royal proclamation from the live site.
Nav: CSS-only hover dropdowns matching live site structure.
Dependencies added
| Package | Purpose |
|---|---|
fast-xml-parser |
XML parsing with CDATA and namespace support |
turndown |
HTML → Markdown conversion |
gray-matter |
Frontmatter parsing at build time |
remark + remark-html |
Markdown → HTML |
sass |
SCSS compilation |
Known content quirks to handle in rendering
- Gallery images — multiple
<img>tags with no captions appear inline in Markdown. A custom renderer could display them as a grid. <br>/ artefacts — Squarespace content sometimes contains empty paragraphs and non-breaking spaces; rendering should collapse these gracefully.- Some posts have no content — only a title and metadata (e.g. very old posts that were images-only on Squarespace).
- Draft posts are included in the output — filter by
status: publishif needed.