🧶 Scoobie
Component library for SEEK documentation sites.
- Author content in Markdown files
- Diagram with mermaid code blocks
- Render content with Braid styling
- Integrate with sku
We use this to build developer.seek.com, among other things.
yarn add --exact scoobie
Table of contents
Setup
sku.config.js
Compile Scoobie and bundle your Markdown content with its Webpack plugin:
const { dangerouslySetWebpackConfig } = require('scoobie/webpack');
module.exports = {
// ...
compilePackages: ['scoobie'],
dangerouslySetWebpackConfig,
};
For detailed usage, see the Webpack reference.
src/render.tsx
Fetch our favourite fonts from our Google overlords, Roboto and Roboto Mono:
import { robotoHtml, robotoMonoHtml } from 'scoobie/typography';
const skuRender: Render<RenderContext> = {
renderDocument: ({ app, bodyTags, headTags }) => `
<!DOCTYPE html>
<html>
<head>
<!-- ... -->
${robotoHtml}
${robotoMonoHtml}
<!-- ... -->
`,
};
If you manually manage the Content Security Policy of your site, you can specify the following sources along with the script hashes from typography.ts:
Content-Security-Policy: font-src https://fonts.gstatic.com; script-src 'sha256-...' 'sha256-...'; style-src https://fonts.googleapis.com
src/scoobie.d.ts
Import TypeScript definitions for MDX
, *.md
and *.mdx
:
import 'scoobie/types';
Markdown reference
Getting started
Scoobie’s Markdown support is powered by MDX and custom Remark plugins.
Create your content in .md
or .mdx
files:
# A normal Markdown heading
Some text...
import { Alert, Text } from 'braid-design-system';
<Alert tone="critical"><Text>And a React component!</Text></Alert>
Import your content into a typical .tsx
file:
import React from 'react';
import Content from './Content.mdx';
export const ContentWithPointlessDiv = () => (
<div>
<Content />
</div>
);
Nest your Markdown components within an MdxProvider:
import 'braid-design-system/reset';
import { BraidProvider } from 'braid-design-system';
import apacTheme from 'braid-design-system/themes/apac';
import React from 'react';
import { MdxProvider, ScoobieLink } from 'scoobie';
import { ContentWithPointlessDiv } from './SomeFile.tsx';
export const App = () => (
<BraidProvider linkComponent={ScoobieLink} theme={apacTheme}>
<MdxProvider>
<ContentWithPointlessDiv />
</MdxProvider>
</BraidProvider>
);
(See React context to learn more about this pattern.)
Diagrams
Scoobie optionally supports simple, source-controlled diagrams via mermaid.
This requires the mermaid
configuration option to be set on ScoobieWebpackPlugin,
and @mermaid-js/mermaid-cli
to be installed as a peer dependency.
From there, the easiest way to get started is to check out the mermaid live editor.
You can use a named code block in Markdown files:
```mermaid Optional title
sequenceDiagram
autonumber
participant P as Partner
participant S as SEEK
P->>S: Make request
```
Or import an .mmd
file like so:
![Diagram](./diagram.mmd 'Optional title')
If you use a separate diagram.mmd
file, you can provide additional mermaid configuration with a diagram.mmd.json
file in the same directory.
Headings
Anchor slugs are automatically generated for h1–h6:
# My Little Heading1
<!-- #my-little-heading1 -->
Images
Vanilla Markdown image syntax is supported:
![Boo raster](./image.png)
![Woo vector](./image.svg)
Define width and height px constraints by overloading the title:
![Alt text](./image.png '=100x20 Rest of title')
Links
Use a root-relative path to navigate to a route managed by your React app. This will be rendered as a React Router link and won't require a full page refresh.
Make sure to point to the route rather than the source file location:
[👍 like this](/root/relative)
[👎 not this](/src/root/Relative.mdx)
Use a full URL to denote an external link. This will open in a new tab.
[Schema reference](https://developer.seek.com/schema)
Tables
Standard Markdown table syntax is supported:
| Default | Left | Centre | Right |
| ------- | :--- | :----: | ----: |
| x | x | x | x |
Use raw HTML to render multiple lines and lists in table cells.
Care must be taken with <p>
s; they are mapped to Braid Text
s, which have strict semantics.
Paragraph tags must be placed around text content and cannot be nested within each other.
| Description | Example |
| :---------------------- | :----------------------------------------------------------------- |
| Single-line | Line |
| Multi-line | <p>Line 1</p><p>Line 2</p> |
| Bullets | <ul><li><p>Bullet 1</p></li></ul> |
| Multi-line with bullets | <p>Line before</p><ul><li><p>Bullet</p></li></ul><p>Line after</p> |
React API reference
Blockquote
Renders rich quoted content.
import { List, Text } from 'braid-design-system';
import React from 'react';
import { Blockquote } from 'scoobie';
export const MyFirstBlockquote = () => (
<Blockquote>
<Text>This is a paragraph.</Text>
<List>
<Text>This is a bullet point.</Text>
</List>
</Blockquote>
);
CodeBlock
Render lines of code with Prism syntax highlighting.
import React from 'react';
import { CodeBlock } from 'scoobie';
export const MyFirstCodeBlock = () => (
<CodeBlock language="javascript">console.log('hello, world');</CodeBlock>
);
CopyableText
Render a Text component that copies the children
string to clipboard on click.
import React from 'react';
import { CodeBlock } from 'scoobie';
export const MyFirstCopyableText = () => (
<CopyableText>This gets copied to clipboard.</CopyableText>
);
InlineCode
Render code inline with text.
import { Text } from 'braid-design-system';
import React from 'react';
import { InlineCode } from 'scoobie';
export const MyFirstInlineCode = () => (
<Text>
Some text with <InlineCode>InlineCode</InlineCode>!
</Text>
);
InternalLink
Render an internal link with the same opinions as our MdxProvider:
- Internal links pass through the
v
or ScoobieLinkProvider URL parameters for UI version switching
Unlike SmartTextLink, this is not bound to a parent Text as it has no underlying TextLink. It can be used to make complex components navigable rather than just sections of text.
import { Stack, Text } from 'braid-design-system';
import React from 'react';
import { InternalLink } from 'scoobie';
export const SomeComplexLinkElement = () => (
<InternalLink href="/page#id" reset>
<Stack space="medium">
<Text>InternalLink supports complex children.</Text>
<Text size="small">It is not bound to a parent Text component.</Text>
</Stack>
</InternalLink>
);
MdxProvider
Provide a base collection of Braid-styled renderers for child MDX documents.
This should be paired with ScoobieLink for proper internal link rendering.
import { BraidProvider, Card } from 'braid-design-system';
import apacTheme from 'braid-design-system/themes/apac';
import React from 'react';
import { MdxProvider, ScoobieLink } from 'scoobie';
import Content from './Content.mdx';
export const Component = () => (
<BraidProvider linkComponent={ScoobieLink} theme={apacTheme}>
<MdxProvider>
<Card>
<Content />
</Card>
</MdxProvider>
</BraidProvider>
);
ScoobieLink
Render all underlying links as follows:
- Internal links pass through the
v
or ScoobieLinkProvider URL parameters for UI version switching - External links open in a new tab
- Links with a
download
attribute prompt the user with a file download
This should be supplied to BraidProvider as the custom linkComponent
:
import { BraidProvider, TextLink } from 'braid-design-system';
import apacTheme from 'braid-design-system/themes/apac';
import React from 'react';
import { ScoobieLink } from 'scoobie';
export const Component = () => (
<BraidProvider linkComponent={ScoobieLink} theme={apacTheme}>
<TextLink href="/root-relative">Internal link</TextLink>
</BraidProvider>
);
ScoobieLinkProvider
Propagate a custom set of URL parameters on internal links.
import { BraidProvider, TextLink } from 'braid-design-system';
import apacTheme from 'braid-design-system/themes/apac';
import React from 'react';
import { ScoobieLink } from 'scoobie';
export const Component = () => (
<ScoobieLinkProvider propagateSearchParams={['debug', 'v']}>
<BraidProvider linkComponent={ScoobieLink} theme={apacTheme}>
<TextLink href="/root-relative">Internal link</TextLink>
</BraidProvider>
</ScoobieLinkProvider>
);
SmartTextLink
Render a text link with the same opinions as our MdxProvider:
- External links open in a new tab and have an IconNewWindow suffix
import { Stack, Text } from 'braid-design-system';
import React from 'react';
import { SmartTextLink } from 'scoobie';
export const SomeLinks = () => (
<Text>
<Stack space="medium">
<SmartTextLink href="/page#id">Scrolls smoothly</SmartTextLink>
<SmartTextLink href="https://developer.seek.com/schema">
Opens in new tab
</SmartTextLink>
</Stack>
</Text>
);
Table
Render an HTML table with the same styling as our MdxProvider:
import React, { Fragment } from 'react';
import { Table, TableRow } from 'scoobie';
export const MyFirstTable = () => (
<Table header={['Column A', 'Column B']}>
<TableRow>
<Fragment>This is body cell A1.</Fragment>
<Fragment>B1</Fragment>
</TableRow>
<TableRow>
<Fragment>A2</Fragment>
<Fragment>This is body cell B2.</Fragment>
</TableRow>
</Table>
);
TableRow
<tr>
component for use with Table.
Row children are flattened then wrapped with <td>
s.
TocRenderer
Render headings from an MDX document with a custom function.
import { Stack, TextLink } from 'braid-design-system';
import React from 'react';
import { TocRenderer } from 'scoobie';
import Content from './Content.mdx';
export const PageWithToc = () => (
<Stack space="medium">
<TocRenderer document={Content}>
{(toc) => (
<Text>
<Stack space="small">
{toc.map((item) => (
<TextLink href={`#${item.id}`} key={item.id}>
{'|'.repeat(item.level)} {item.children}
</TextLink>
))}
</Stack>
</Text>
)}
</TocRenderer>
<Content />
</Stack>
);
A heading must start at the beginning of its line to be parsed:
# Good
- ## Bad
- ### Super bad
## Good again
(This can be enforced with markdownlint’s MD023 rule.)
WrapperRenderer
Render an MDX document with a customised wrapper.
This allows you to derive arbitrary components from select parts of the document.
import { Text } from 'braid-design-system';
import React, { Children } from 'react';
import { WrapperRenderer } from 'scoobie';
export const NodeCount = (Document: MDX.Document) => (
<WrapperRenderer document={Document}>
{({ children }) => (
<Text>{Children.toArray(children).length} top-level node(s)</Text>
)}
</WrapperRenderer>
);
Styling reference
Scoobie distributes some vanilla-extract styles via scoobie/styles
submodules.
code
Render text with the same monospace styling as our CodeBlock:
import { Box } from 'braid-design-system';
import React from 'react';
import { code } from 'scoobie/styles/code.css';
export const MyBox = () => (
<Box className={code.standard}>
<Box component="pre">Hello</Box>
</Box>
);
img
Render an image with the same styling as our MdxProvider:
import React from 'react';
import { img } from 'scoobie/styles/img.css';
export const MySvg = () => (
<svg className={img}>
<path />
</svg>
);
Webpack reference
Scoobie distributes its Webpack config via a scoobie/webpack
submodule:
const { ScoobieWebpackPlugin } = require('scoobie/webpack');
Compatibility notes:
-
SVGs cannot be directly imported into JSX as components.
Consider inlining the SVGs in your JSX instead.
ScoobieWebpackPlugin
A bundle of MDX and image loaders that complement sku's Webpack config.
This needs to be ordered to run after SkuWebpackPlugin:
const { ScoobieWebpackPlugin, merge } = require('scoobie/webpack');
module.exports = {
// ...
compilePackages: ['scoobie'],
dangerouslySetWebpackConfig: (config) =>
merge(config, {
plugins: [
new ScoobieWebpackPlugin({
// Optional configuration option to enable mermaid support.
// `@mermaid-js/mermaid-cli` must be installed as a peer dependency.
// Temporary files are written to `${rootDir}/mermaid`.
mermaid: {
rootDir: __dirname,
},
}),
],
}),
};
dangerouslySetWebpackConfig
Zero-config option referenced in sku.config.js above.
This slots in on top of sku without much fuss. If you're wrangling other Webpack config and need something more composable, see ScoobieWebpackPlugin.
merge
Re-export of webpack-merge for convenience.