Skip to content

Commit 6bfb0f6

Browse files
Retire the homebrew CMS UI; render Wagtail content on the public site
Deletes the in-app admin (/admin dashboard, comment review, thumbnails) and the whole TipTap editor stack — the CMS owns all of it now — and switches the public pages to the Wagtail JSON API: StreamRenderer walks StreamField blocks where RichTextRenderer walked ProseMirror nodes, and portal / place / static routes fetch from /api/content/. Net -4400 lines. The dynamic blocks keep parity with the deleted TipTap node views (plan gallery, comment gallery, submission form, map-create buttons, boilerplate, section header), and map-create buttons now render the same cards place pages use. tiptap and auth0 packages leave the lockfile. Auth0 login still works after this PR — it is simply no longer used by anything except the login page itself, which the cutover PR replaces. Static pages are served by a /[slug] catch-all, so a hardcoded route always wins over a CMS page with the same slug: migrate them one at a time by deleting the route and publishing the page.
1 parent f03f1c6 commit 6bfb0f6

67 files changed

Lines changed: 375 additions & 4964 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/bun.lock

Lines changed: 9 additions & 178 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/e2e/utils/network-helpers.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export const API_ENDPOINTS = {
1818
patchUnshatter: '**/api/unshatter/*',
1919
root: '**/', // Returns {"message":"Hello World"}
2020
districtrMaps: '**/api/districtr_maps',
21-
cmsContent: '**/api/cms/*',
2221
} as const;
2322

2423
function globToRegExp(glob: string): RegExp {

app/package.json

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
"test:e2e:preview": "BASE_URL=$PREVIEW_URL playwright test"
2020
},
2121
"dependencies": {
22-
"@auth0/nextjs-auth0": "^4.13.0",
2322
"@emotion/react": "^11.13.0",
2423
"@emotion/styled": "^11.13.0",
2524
"@mapbox/mapbox-gl-sync-move": "^0.3.1",
@@ -35,14 +34,6 @@
3534
"@sentry/nextjs": "9.1.0",
3635
"@stitches/react": "^1.2.8",
3736
"@tanstack/react-query": "^5.51.11",
38-
"@tiptap/extension-color": "^2.11.5",
39-
"@tiptap/extension-image": "^2.11.5",
40-
"@tiptap/extension-link": "^2.11.5",
41-
"@tiptap/extension-text-style": "^2.11.5",
42-
"@tiptap/extension-underline": "^2.11.5",
43-
"@tiptap/html": "^2.11.5",
44-
"@tiptap/react": "^2.11.5",
45-
"@tiptap/starter-kit": "^2.11.5",
4637
"@turf/turf": "^7.2.0",
4738
"@types/d3-scale": "^4.0.9",
4839
"@types/mdx": "^2.0.13",
@@ -76,6 +67,7 @@
7667
"maplibre-gl": "^4.7.1",
7768
"nanoid": "^5.1.2",
7869
"next": "16.0.10",
70+
"next-auth": "5.0.0-beta.31",
7971
"papaparse": "^5.5.2",
8072
"pmtiles": "^3.0.7",
8173
"polylabel": "^2.0.1",
@@ -90,7 +82,6 @@
9082
"sharp": "^0.34.1",
9183
"simple-statistics": "^7.8.7",
9284
"superjson": "^2.2.1",
93-
"tiptap": "^1.32.2",
9485
"topojson-client": "^3.1.0",
9586
"zundo": "^2.3.0",
9687
"zustand": "4.5.2"
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import {LanguagePicker} from '@/app/components/LanguagePicker/LanguagePicker';
2+
import StreamRenderer from '@/app/components/RichTextRenderer/StreamRenderer';
3+
import {getCMSContent} from '@/app/utils/api/cmsContent';
4+
import {Flex, Heading} from '@radix-ui/themes';
5+
import {cookies} from 'next/headers';
6+
import {notFound} from 'next/navigation';
7+
8+
// Catch-all for CMS-authored static pages. Hardcoded routes (about/, rules/,
9+
// ...) take precedence in Next.js routing, so pages migrate into the CMS one
10+
// at a time: delete the hardcoded route and publish a StaticPage of the same
11+
// slug.
12+
export const revalidate = 3600;
13+
14+
export async function generateMetadata({params}: {params: Promise<{slug: string}>}) {
15+
const [{slug}, userCookies] = await Promise.all([params, cookies()]);
16+
const language = userCookies.get('language')?.value ?? 'en';
17+
const cmsData = await getCMSContent('static', slug, language).catch(() => null);
18+
const title = cmsData?.content?.title;
19+
return title ? {title} : {};
20+
}
21+
22+
export default async function Page({params}: {params: Promise<{slug: string}>}) {
23+
const [{slug}, userCookies] = await Promise.all([params, cookies()]);
24+
const language = userCookies.get('language')?.value ?? 'en';
25+
const cmsData = await getCMSContent('static', slug, language).catch(() => null);
26+
27+
if (!cmsData?.content) {
28+
notFound();
29+
}
30+
31+
return (
32+
<Flex direction="column" width="100%" py="6">
33+
<Heading as="h1" size="6" mb="4">
34+
{cmsData.content.title}
35+
</Heading>
36+
<LanguagePicker
37+
preferredLanguage={language}
38+
availableLanguages={cmsData.available_languages}
39+
/>
40+
<StreamRenderer body={cmsData.content.body} className="my-4" />
41+
</Flex>
42+
);
43+
}

app/src/app/(static)/place/[slug]/page.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import {LanguagePicker} from '@/app/components/LanguagePicker/LanguagePicker';
2-
import RichTextRenderer from '@/app/components/RichTextRenderer/RichTextRenderer';
2+
import StreamRenderer from '@/app/components/RichTextRenderer/StreamRenderer';
33
import {ContentSection} from '@/app/components/Static/ContentSection';
44
import {PlaceMapGrid} from '@/app/components/Static/Interactions/PlaceMapGrid';
55
import {getAvailableDistrictrMaps} from '@/app/utils/api/apiHandlers/getAvailableDistrictrMaps';
6-
import {getCMSContent} from '@/app/utils/api/cms';
6+
import {getCMSContent} from '@/app/utils/api/cmsContent';
77
import {Flex, Heading} from '@radix-ui/themes';
88
import {cookies} from 'next/headers';
99

@@ -12,20 +12,20 @@ export const revalidate = 3600;
1212
export async function generateMetadata({params}: {params: Promise<{slug: string}>}) {
1313
const [{slug}, userCookies] = await Promise.all([params, cookies()]);
1414
const language = userCookies.get('language')?.value ?? 'en';
15-
const cmsData = await getCMSContent(slug, language, 'places').catch(() => null);
16-
const title = cmsData?.content?.published_content?.title;
15+
const cmsData = await getCMSContent('places', slug, language).catch(() => null);
16+
const title = cmsData?.content?.title;
1717
return title ? {title, description: `Draw and explore districting maps for ${title}`} : {};
1818
}
1919

2020
export default async function Page({params}: {params: Promise<{slug: string}>}) {
2121
const [{slug}, userCookies] = await Promise.all([params, cookies()]);
2222
const language = userCookies.get('language')?.value ?? 'en';
2323
const [cmsData, maps] = await Promise.all([
24-
getCMSContent(slug, language, 'places'),
24+
getCMSContent('places', slug, language),
2525
getAvailableDistrictrMaps({}),
2626
]).catch(() => [null, null]);
2727

28-
if (!cmsData?.content?.published_content || !maps) {
28+
if (!cmsData?.content || !maps) {
2929
return (
3030
<Flex className="size-full" justify="center" align="center">
3131
<Heading>Content not found</Heading>
@@ -43,7 +43,7 @@ export default async function Page({params}: {params: Promise<{slug: string}>})
4343
return (
4444
<Flex direction="column" width="100%" pt="4">
4545
<Heading as="h1" size="6" mb="4">
46-
{cmsData.content.published_content.title}
46+
{cmsData.content.title}
4747
</Heading>
4848
<LanguagePicker
4949
preferredLanguage={language}
@@ -53,7 +53,7 @@ export default async function Page({params}: {params: Promise<{slug: string}>})
5353
{Boolean(availableMaps?.length) && <PlaceMapGrid maps={availableMaps!} />}
5454
</ContentSection>
5555

56-
<RichTextRenderer content={cmsData.content.published_content.body} className="my-4" />
56+
<StreamRenderer body={cmsData.content.body} className="my-4" />
5757
</Flex>
5858
);
5959
}

app/src/app/(static)/places/page.tsx

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {listCMSContent, PlacesCMSContent} from '@/app/utils/api/cms';
1+
import {listCMSContent} from '@/app/utils/api/cmsContent';
22
import {fastUniqBy} from '@/app/utils/arrays';
33
import {Card, Flex, Grid, Heading, Text, Link} from '@radix-ui/themes';
44

@@ -12,14 +12,9 @@ export const metadata = {
1212

1313
export default async function TagsPage() {
1414
const cmsContent = await listCMSContent('places');
15-
const cmsContentWithPublishedContent = cmsContent?.filter(content => content.published_content);
16-
if (!cmsContentWithPublishedContent) return null;
15+
if (!cmsContent) return null;
1716

18-
const entries = fastUniqBy(cmsContentWithPublishedContent, 'slug').sort((a, b) =>
19-
a.published_content!.title.localeCompare(b.published_content!.title)
20-
) as PlacesCMSContent[];
21-
22-
if (!entries) return null;
17+
const entries = fastUniqBy(cmsContent, 'slug').sort((a, b) => a.title.localeCompare(b.title));
2318

2419
return (
2520
<Flex direction={'column'}>
@@ -35,21 +30,20 @@ export default async function TagsPage() {
3530
gap="4"
3631
>
3732
{entries.length === 0 && <Text>No places available.</Text>}
38-
{entries.map(content => (
39-
<Card key={content.slug}>
40-
<Heading as="h3" size="4">
41-
{content.published_content!.title}
42-
</Heading>
43-
{!!(content?.districtr_map_slugs && content?.districtr_map_slugs?.length) && (
44-
<Text>
45-
{content.districtr_map_slugs.length} map module
46-
{content.districtr_map_slugs.length === 1 ? '' : 's'}
33+
{entries.map(content => {
34+
const moduleCount = content.districtr_map_slugs?.length ?? 0;
35+
return (
36+
<Card key={content.slug}>
37+
<Heading as="h3" size="4">
38+
{content.title}
39+
</Heading>
40+
<Text as="p" size="2" color="gray">
41+
{moduleCount} map module{moduleCount === 1 ? '' : 's'}
4742
</Text>
48-
)}
49-
<br />
50-
<Link href={`/place/${content.slug}`}>Go to place</Link>
51-
</Card>
52-
))}
43+
<Link href={`/place/${content.slug}`}>Go to place</Link>
44+
</Card>
45+
);
46+
})}
5347
</Grid>
5448
</Flex>
5549
);
Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import {HeaderSecondTierNav} from '@/app/components/Cms/RichTextEditor/extensions/HeaderSecondTierNav/HeaderSecondTierNav';
22
import {LanguagePicker} from '@/app/components/LanguagePicker/LanguagePicker';
3-
import RichTextRenderer from '@/app/components/RichTextRenderer/RichTextRenderer';
3+
import StreamRenderer from '@/app/components/RichTextRenderer/StreamRenderer';
44
import {getAvailableDistrictrMaps} from '@/app/utils/api/apiHandlers/getAvailableDistrictrMaps';
5-
import {getCMSContent} from '@/app/utils/api/cms';
5+
import {getCMSContent} from '@/app/utils/api/cmsContent';
66
import {Flex, Heading} from '@radix-ui/themes';
77
import {cookies} from 'next/headers';
88

@@ -11,20 +11,20 @@ export const revalidate = 3600;
1111
export async function generateMetadata({params}: {params: Promise<{slug: string}>}) {
1212
const [{slug}, userCookies] = await Promise.all([params, cookies()]);
1313
const language = userCookies.get('language')?.value ?? 'en';
14-
const cmsData = await getCMSContent(slug, language, 'tags').catch(() => null);
15-
const title = cmsData?.content?.published_content?.title;
14+
const cmsData = await getCMSContent('tags', slug, language).catch(() => null);
15+
const title = cmsData?.content?.title;
1616
return title ? {title} : {};
1717
}
1818

1919
export default async function Page({params}: {params: Promise<{slug: string}>}) {
2020
const [{slug}, userCookies] = await Promise.all([params, cookies()]);
2121
const language = userCookies.get('language')?.value ?? 'en';
2222
const [cmsData, maps] = await Promise.all([
23-
getCMSContent(slug, language, 'tags'),
23+
getCMSContent('tags', slug, language),
2424
getAvailableDistrictrMaps({}),
2525
]).catch(() => [null, null]);
2626

27-
if (!cmsData?.content?.published_content || !maps) {
27+
if (!cmsData?.content || !maps) {
2828
return (
2929
<Flex className="size-full" justify="center" align="center">
3030
<Heading>Content not found</Heading>
@@ -33,16 +33,16 @@ export default async function Page({params}: {params: Promise<{slug: string}>})
3333
}
3434

3535
return (
36-
<Flex direction="column" width="100%" pt="6">
36+
<Flex direction="column" width="100%" py="6">
3737
<Heading as="h1" size="6" mb="4">
38-
{cmsData.content.published_content.title}
38+
{cmsData.content.title}
3939
</Heading>
4040
<LanguagePicker
4141
preferredLanguage={language}
4242
availableLanguages={cmsData.available_languages}
4343
/>
4444
<HeaderSecondTierNav />
45-
<RichTextRenderer content={cmsData.content.published_content.body} className="my-4" />
45+
<StreamRenderer body={cmsData.content.body} className="my-4" />
4646
</Flex>
4747
);
4848
}

app/src/app/(static)/portals/page.tsx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {listCMSContent, PlacesCMSContent} from '@/app/utils/api/cms';
1+
import {listCMSContent} from '@/app/utils/api/cmsContent';
22
import {fastUniqBy} from '@/app/utils/arrays';
33
import {Card, Flex, Grid, Heading, Text, Link} from '@radix-ui/themes';
44

@@ -12,14 +12,9 @@ export const metadata = {
1212

1313
export default async function PortalsPage() {
1414
const cmsContent = await listCMSContent('tags');
15-
const cmsContentWithPublishedContent = cmsContent?.filter(content => content.published_content);
16-
if (!cmsContentWithPublishedContent) return null;
15+
if (!cmsContent) return null;
1716

18-
const entries = fastUniqBy(cmsContentWithPublishedContent, 'slug').sort((a, b) =>
19-
a.published_content!.title.localeCompare(b.published_content!.title)
20-
) as PlacesCMSContent[];
21-
22-
if (!entries) return null;
17+
const entries = fastUniqBy(cmsContent, 'slug').sort((a, b) => a.title.localeCompare(b.title));
2318

2419
return (
2520
<Flex direction={'column'} pt="6">
@@ -38,7 +33,7 @@ export default async function PortalsPage() {
3833
{entries.map(content => (
3934
<Card key={content.slug}>
4035
<Heading as="h3" size="4">
41-
{content.published_content!.title}
36+
{content.title}
4237
</Heading>
4338
<Link href={`/portal/${content.slug}`}>Go to portal</Link>
4439
</Card>

app/src/app/admin/cms/[type]/CmsPage.tsx

Lines changed: 0 additions & 36 deletions
This file was deleted.

app/src/app/admin/cms/[type]/page.tsx

Lines changed: 0 additions & 7 deletions
This file was deleted.

0 commit comments

Comments
 (0)