Gatsby was a common target for content-driven sites, so the design system needed a first-class path into it.
This theme wraps kickstartDS components for Gatsby: a theme package plus an example site, so a project can pull the design system in and start composing pages.
| Fact | Detail |
|---|---|
| Type | Gatsby theme + example workspace |
| Stack | JavaScript, Gatsby, React |
| Started | February 2021 |
What's in it
The example site is the documentation: how to wire the theme into a project.
Part of making the same design system available wherever content lives — Gatsby, Next.js or plain HTML.
In detail
What is actually inside it
The example ships example/content/ with MDX pages including de-datenschutz.md and de/settings/* — which is how a theme gets tested against real content shapes instead of fixtures.
.autorc (auto) plus CircleCI: changelog and release management came from commit conventions, matching how the CLI and schema tooling were run.
| Detail | Value |
|---|---|
| Type | Gatsby theme + example site |
| Interface | GraphQL (`.graphqlrc.yml`) |
| Started | February 2021 |
Technical notes
The decisions behind it
Why a Gatsby theme at all?
What is the theme's real contract?
The generated GraphQL layer
The theme's real interface is a schema it does not ship
Nothing here hand-writes the GraphQL types its components receive. They are generated from the design system's JSON Schemas and consumed as a build input:
@kickstartds/generate-from-schemaemitsdist/page.graphqlanddist/config.yml;- GraphQL Codegen, configured in
.graphqlrc.yml, reprocesses that schema back intodist/page.graphql(adding the Gatsby directives@dontInfer,@link,@dateformat) and emitsdist/page.fragments.jsthrough@kickstartds/graphql-cli-generate-fragments; - the root
yarn generatescript then copies both intoexample/dist/, and the generated CMS config intoexample/static/admin/config.yml.
The theme reads ${gqlPath}/page.graphql — default dist — inside createSchemaCustomization and hands the text straight to createTypes(). No published package ships that file, so a site that skips the generator fails on a missing schema before it has read a single content file. (netlifyConfigPath is accepted as an option and defaulted to dist but never read; the copy lives in the root script instead.)
Keys arrive hashed. The JSON-Schema→GraphQL step appends a suffix to avoid collisions, so nodes look like activeEntry__254f, sections__17ac or headline__b113. componentMapper.tsx passes those names through to components and calls cleanObjectKeys from @kickstartds/jsonschema2graphql/build/dehashing to strip them again — the theme is coupled to the tool's hashing and its de-hashing, both reached through an undeclared build/ subpath. Two unions cannot be generated: SectionComponentContent and TextMediaComponentMedia are assembled by hand in gatsby-node.js, both with resolveType: (value) => pascalCase(value.type) + 'Component' and the same // TODO generalize this.
Because a section's content is another component that may itself contain sections, queries cannot be hand-written either. collectGraphQLFragments.js imports Gatsby's private parser (gatsby/dist/query/file-parser) and walks FragmentSpread nodes recursively to inline the transitive closure of named fragments into a query string. createPages.js names sixteen component fragments explicitly just to fill allKickstartDsPage, next to a // TODO dedupe the components fragments below, this should not be necessary!. Five more page queries are regenerated on disk at build time: gatsby-node.js reads its own src/pages/{kickstartDs*Page.slug}.js collection-route templates, regex-replaces everything from export const query onward, and writes the files back.
| Artifact | Produced by | Consumed at |
|---|---|---|
| dist/page.graphql | @kickstartds/generate-from-schema, then Codegen injects the Gatsby directives | createSchemaCustomization → createTypes() |
| dist/page.fragments.js | GraphQL Codegen (@kickstartds/graphql-cli-generate-fragments) | collectGraphQLFragments() |
| dist/config.yml | generate-from-schema | copied to static/admin/config.yml (Netlify CMS) |
| src/pages/{kickstartDs*Page.slug}.js | rewritten in place by createPages() | Gatsby page queries |
| src/components/ComponentsMap.ts | postinstall (scripts/createComponentsMap.js) | componentMapper.tsx |
One interface, four sources
Where page nodes actually come from
The contentPath directory is plain gatsby-source-filesystem (source name urlSegment). Everything above it is one transformer package per CMS, each minting the shapes the theme queries:
- Markdown + Netlify CMS — a
MarkdownRemarkwhose frontmatter has anidbecomes aKickstartDsContentPage; files under asettings/folder named*header.md/*footer.mdbecome singleKickstartDsHeader/KickstartDsFooternodes instead. Language variants are just more files, socontent/de/settings/header.mdsits besidecontent/settings/header.md. - MDX —
Mdxnodes becomeKickstartDsMdxBlogPage, withpostHead,postBody,postAside,postReadingTimeandpostWordCountderived from frontmatter. - WordPress —
WpPostbecomesKickstartDsWordpressBlogPagewith ablog/-prefixed slug,layout: 'blog-detail', and reading time at 140 words per minute. - Contentful —
ContentfulTerm,ContentfulAppearance,ContentfulShowcaseandContentfulTagbecome the glossary, appearance, showcase and tag pages.
Seven page types implement one KickstartDsPage interface (@dontInfer, implements Node), alongside the two singleton KickstartDsHeader / KickstartDsFooter nodes that do not. That interface is what lets a single allKickstartDsPage query drive generic page creation. The blog is the outlier: three separate implementations — MDX, WordPress and Contentful — satisfy the same KickstartDsBlogPage interface, so a site can swap sources without touching the list or detail templates.
The transformers are not thin adapters. The WordPress author resolver looks up a WpUser, then finds a ContentfulPerson with the same name, logging 'Missing ContentfulPerson for author' when that fails. The Contentful plugin invokes a Supabase edge function (functions.invoke('description', ...) with GATSBY_SUPABASE_URL / GATSBY_SUPABASE_ANON_KEY) to write tag descriptions during the build, and derives glossary source thumbnails from a public Supabase storage bucket. Missing assets are almost always a console.log plus return undefined rather than a thrown error, so a build can complete with holes and only surface them later as a GraphQLDocumentError.
| Node type | Created from | layout |
|---|---|---|
| KickstartDsContentPage | MarkdownRemark with a frontmatter id | default |
| KickstartDsHeader / KickstartDsFooter | settings/*header.md, settings/*footer.md | — |
| KickstartDsMdxBlogPage | Mdx | blog-detail |
| KickstartDsWordpressBlogPage | WpPost | blog-detail |
| KickstartDsGlossaryPage | ContentfulTerm | glossary |
| KickstartDsAppearancePage | ContentfulAppearance | appearance |
| KickstartDsShowcasePage | ContentfulShowcase | showcase |
| KickstartDsTagPage | ContentfulTag | tag-label |
The example is the documentation
And the only place the shadowing contract is exercised
Both READMEs are the wrong ones. The root README still describes the workspace as a Gatsby starter (gatsby-theme-kickstartDS/gatsby-config.js: 'An empty gatsby-config that you can use as a starting point'); the theme's own README is the unmodified 'The smallest possible Gatsby theme' scaffold text; and example/README.md is the WordPress Twenty Twenty starter readme, complete with WPGraphQL and menu-slug setup steps the example never uses. The working documentation is the example site.
That makes example/ load-bearing: it is a real site, not fixtures, and it is the only place the theme's component-shadowing contract is exercised. Five files under example/src/@kickstartds/gatsby-theme-kickstartds/components/ shadow theme internals — Layout, Providers, ScrollSpy, BlogDetailPage and BlogListPage. Two are instructive:
- the shadowed
Layoutadds<IconSprite/>,<Header/>and<Footer/>, because the theme's ownLayoutrenders only<main tabIndex={-1}>plus a provider stack; - the shadowed
Providerswraps the theme's providers inButtonProvider,HeadlineProvider,CountUpProvider,VisualProvider,StorytellingProviderandSectionProviderfrom@kickstartds/design-system, andLayoutimports that package'sdist/index.cssanddist/index.js.
So the theme deliberately contains neither the design system's runtime nor its CSS: the site adds @kickstartds/design-system itself, and the root yarn bundle script copies node_modules/@kickstartds/design-system/dist/* into example/static/. The example enables three of the four transformers — MDX is present but commented out — yet the theme's Layout imports the MDX transformer's src/shiki-twoslash.css anyway, a stylesheet that resolves only because the monorepo hoists the sibling workspace. The example's own content is the test bed: content/de-datenschutz.md sits outside the de/ directory while claiming slug: de/datenschutz.
postinstall runs scripts/createComponentsMap.js: it walks @kickstartds/{base,blog,content}'s exports.json, union-diffs any other package that ships a dist/exports.json (found by globbing node_modules), and writes a gitignored ComponentsMap.ts full of loadable(() => import(...)) entries. It hardcodes the CI checkout path /mnt/ramdisk/ and otherwise guesses the relative depth. There is no committed registry to read.
patches/gatsby-remark-shiki-twoslash+3.0.34.patch and patches/photoswipe+5.3.7.patch are applied by patch-package on postinstall, while root resolutions pin @kickstartds/base|blog|core|form to 2.0.4-next.* and @kickstartds/content to a canary. The MDX transformer feeds Twoslash a virtual filesystem containing @types/react/index.d.ts and GlossaryComponent.d.ts, so fenced code in blog posts is type-checked at build time (theme: 'material-default').
componentAnalytics.js writes per-component usage points to InfluxDB from GATSBY_INFLUX_*, but both call sites are commented out behind // TODO re-activate: this is the entry point for usage based component metrics. The MDX postBio resolver hardcodes two team bios with a // TODO make dynamic again, and computes postReadingTime from getMarkdownExcerpt(node.rawBody) — the first 300 characters only.
content/blog/demo.mdx carries an Internal category that createBlogList.js filters out of the listing, described in-code as 'currently used to have posts generated, but not on the list'. The German footer is detected by whether sections__17ac[1].headline__b113 contains 'Kontakt' — the content shape is the configuration.
Sharp edges
What the strings, the writes and the packaging actually cost
Why does a page whose slug mentions 'blog' disappear?
createPages.js skips any page whose slug includes blog, glossary, showcases or appearances — substring checks, not prefixes — because the three list creators and the {Type.slug}.js collection routes own those paths, so a content page like coding/weblog-notes would be dropped silently. Language selection is string-matching too: a page is German when slug.includes('de/') || slug === 'de' || slug.includes('ueber-uns') (which also matches any slug containing de/), the header is chosen by activeEntry__254f.includes('de'), and the footer by the 'Kontakt' probe above. Because the field names come from the generated schema, changing a content shape means changing these probes.What breaks if I copy the components in instead of using the theme?
page.graphql and fragments plus the Codegen step that produces them; collectGraphQLFragments and its recursive fragment inlining; the cleanObjectKeys de-hashing (props would arrive as activeEntry__254f); the ComponentsMap.ts generated at postinstall with its @loadable/component SSR plugin; the resolveType bridges for SectionComponentContent and TextMediaComponentMedia; the remark and Sharp pipeline from the theme's gatsby-config (quality 50, breakpoints 750/1080/1366/1920, webp + auto, blurred placeholders, gatsby-remark-relative-source htmlSources for <post-video image>); and the four transformer packages that turn Markdown, Mdx, WpPost and Contentful nodes into page nodes. The parts you would have to rebuild are exactly the parts that are not components.How were releases run — and are the packages actually public?
.autorc: the npm and released plugins, baseBranch: master, versionBranches: support/) over Lerna in independent mode. CircleCI's release job uses cimg/node:18.12, works in /mnt/ramdisk on an xlarge runner with caches keyed on yarn.lock plus the config file, writes NPM_TOKEN into a ramdisk .npmrc, runs yarn install --frozen-lockfile, removes the .npmrc, then runs yarn auto shipit with the token coming from the kickstartDS context. The changelog is dominated by 'Pushed to master' entries carrying the raw commit subject, and bumps stayed in patch range (theme 2.5.6 → 2.5.9). Despite the root README describing installation from npm, every package sets license: UNLICENSED and publishConfig.access: restricted; the Contentful space id 6g9liq51xuol is hardcoded in the transformer's gatsby-config.js, with only the token coming from the environment.Why does the build write into its own package?
src/pages/{kickstartDs*Page.slug}.js files ship with a placeholder export const query, and createPages rewrites them in place before creating pages. In the monorepo that mutates the working tree; installed from npm it writes inside node_modules, which fights immutable installs (pnpm, Yarn PnP) and caches keyed on node_modules — the same thing CircleCI restores. The schema it depends on is also supplied from outside the package: yarn generate copies page.graphql, page.fragments.js and the CMS config into the site.What must the site supply that the theme does not?
Providers is a bare wrapper; the example shadows it to add six design-system providers, imports dist/index.css / dist/index.js, and mounts IconSprite, <Header/> and <Footer/> in the shadowed Layout. example/gatsby-config.js then adds everything else the theme does not: siteMetadata (including siteUrl, which the imageUrl resolvers need to build absolute URLs), trailingSlash: 'always', gatsby-plugin-advanced-sitemap with a hand-written query and per-type mapping, and three of the four transformers. The theme sets the options it does take — contentPath, urlSegment, gqlPath — to the values the example passes back verbatim.Links
Where to find this one
