Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ dist/
storybook-static/
next-env.d.ts

packages/icons-react/icons.tsx

# editors
.idea/
Expand All @@ -17,5 +18,4 @@ next-env.d.ts

# Secrets
.FIGMA_TOKEN
.vercel
packages/icons-react/icons.tsx
.env.local
2 changes: 2 additions & 0 deletions apps/docs/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Creata a viewer token at sanity.io/manage
SANITY_VIEWER_TOKEN=
4 changes: 3 additions & 1 deletion apps/docs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@
public/resources/icons/
public/storybook
component-props.ts
public/studio/static
public/studio/static
docgen.ts
.env
97 changes: 97 additions & 0 deletions apps/docs/app/routes/_docs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { previewMiddleware } from '@/lib/preview-middleware';
import { sanityFetch } from '@/lib/sanity';
import { VisualEditing } from '@/lib/visual-editing';
import appCss from '@/styles/app.css?url';
import { DisablePreviewMode } from '@/ui/disable-preview-mode';
import { Footer } from '@/ui/footer';
import { MainNav } from '@/ui/main-nav';
import { GrunnmurenProvider } from '@obosbbl/grunnmuren-react';
import {
type NavigateOptions,
Outlet,
ScrollRestoration,
type ToOptions,
createFileRoute,
useRouter,
} from '@tanstack/react-router';
import { createServerFn } from '@tanstack/start';
import { defineQuery } from 'groq';

const COMPONENTS_NAVIGATION_QUERY = defineQuery(
// make sure the slug is always a string so we don't have add fallback value in code just to make TypeScript happy
`*[_type == "component"]{ _id, name, 'slug': coalesce(slug.current, '')} | order(name asc)`,
);

const checkIsPreview = createServerFn({ method: 'GET' })
.middleware([previewMiddleware])
.handler(({ context }) => {
return context.previewMode;
});

// This is the shared layout for all the Grunnmuren docs pages that are "public", ie not the Sanity studio
export const Route = createFileRoute('/_docs')({
component: RootLayout,
head: () => ({
links: [{ rel: 'stylesheet', href: appCss }],
meta: [
{
title: "Grunnmuren - OBOS' Design System",
},
],
}),
beforeLoad: async () => {
const isPreview = await checkIsPreview();
return { isPreview };
},
loader: async ({ context }) => {
return {
componentsNavItems: (
await sanityFetch({ query: COMPONENTS_NAVIGATION_QUERY })
).data,
isPreview: context.isPreview,
};
},
});

function RootLayout() {
const router = useRouter();
const { isPreview } = Route.useLoaderData();

return (
<>
<GrunnmurenProvider
locale="nb"
// This integrates RAC/Grunnmuren with TanStack router
// Giving us typesafe routes
// See https://react-spectrum.adobe.com/react-aria/routing.html#tanstack-router
navigate={(to, options) => router.navigate({ to, ...options })}
useHref={(to) => router.buildLocation({ to }).href}
>
{isPreview && (
<>
<VisualEditing />
<DisablePreviewMode />
</>
)}
<div className="grid min-h-screen lg:flex">
<div className="flex grow flex-col px-6">
<main className="grow">
<Outlet />
</main>
<Footer />
</div>
<MainNav />
</div>
</GrunnmurenProvider>
<ScrollRestoration />
</>
);
}

// See comments on GrunnmurenProvider in <RootLayout />
declare module 'react-aria-components' {
interface RouterConfig {
href: ToOptions['to'];
routerOptions: Omit<NavigateOptions, keyof ToOptions>;
}
}
54 changes: 54 additions & 0 deletions apps/docs/app/routes/_docs/komponenter/$slug.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as badgeExamples from '@/examples/badge';
import * as buttonExamples from '@/examples/button';
import { sanityFetch } from '@/lib/sanity.server';
import { Content } from '@/ui/content';
import { PropsTable } from '@/ui/props-table';
import { createFileRoute, notFound } from '@tanstack/react-router';
import * as props from 'docgen';
import { defineQuery } from 'groq';

const COMPONENT_QUERY = defineQuery(
`*[_type == "component" && slug.current == $slug][0]{ content, "name": coalesce(name, '') }`,
);

export const Route = createFileRoute('/_docs/komponenter/$slug')({
component: Page,
loader: async ({ params, context }) => {
console.log('context in component route', context);
const res = await sanityFetch({
data: {
query: COMPONENT_QUERY,
params: { slug: params.slug },
},
});

if (res.data == null) {
throw notFound();
}

const componentName = res.data.name;
const componentProps = props[componentName].props;

return { data: res.data, componentProps };
},
});

function Page() {
const { data, componentProps } = Route.useLoaderData();

// @ts-expect-error this works for now until we figure how to make the examples work better with Sanity
const { scope, examples } = {

Check warning on line 40 in apps/docs/app/routes/_docs/komponenter/$slug.tsx

View workflow job for this annotation

GitHub Actions / build (24.x)

eslint(no-unused-vars)

Variable 'examples' is declared but never used. Unused variables should start with a '_'.

Check warning on line 40 in apps/docs/app/routes/_docs/komponenter/$slug.tsx

View workflow job for this annotation

GitHub Actions / build (24.x)

eslint(no-unused-vars)

Variable 'scope' is declared but never used. Unused variables should start with a '_'.
Button: buttonExamples,
Badge: badgeExamples,
}[data.name];

return (
<>
<h1 className="heading-l mb-12 mt-9">{data.name}</h1>

<Content content={data.content ?? []} />

<PropsTable props={componentProps} />
</>
);
}
14 changes: 14 additions & 0 deletions apps/docs/app/routes/api/preview-mode/disable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createAPIFileRoute } from '@tanstack/start/api';
import { deleteCookie, sendRedirect } from 'vinxi/http';

export const APIRoute = createAPIFileRoute('/api/preview-mode/disable')({
GET: () => {
deleteCookie('__sanity_preview', {
path: '/',
secure: import.meta.env.PROD,
httpOnly: true,
sameSite: 'strict',
});
sendRedirect('/');
},
});
37 changes: 37 additions & 0 deletions apps/docs/app/routes/api/preview-mode/enable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { randomBytes } from 'node:crypto';

Check warning on line 1 in apps/docs/app/routes/api/preview-mode/enable.ts

View workflow job for this annotation

GitHub Actions / build (24.x)

eslint(no-unused-vars)

Identifier 'randomBytes' is imported but never used.
import { client } from '@/lib/sanity';
import { validatePreviewUrl } from '@sanity/preview-url-secret';
import { createAPIFileRoute } from '@tanstack/start/api';
import { SanityClient } from 'sanity';

Check warning on line 5 in apps/docs/app/routes/api/preview-mode/enable.ts

View workflow job for this annotation

GitHub Actions / build (24.x)

eslint(no-unused-vars)

Identifier 'SanityClient' is imported but never used.
import { sendRedirect, setCookie } from 'vinxi/http';

export const APIRoute = createAPIFileRoute('/api/preview-mode/enable')({
GET: async ({ request }) => {
if (!process.env.SANITY_VIEWER_TOKEN) {
throw new Response('Preview mode missing token', { status: 401 });
}

const clientWithToken = client.withConfig({
token: process.env.SANITY_VIEWER_TOKEN,
});

const { isValid, redirectTo = '/' } = await validatePreviewUrl(
clientWithToken,
request.url,
);

if (!isValid) {
throw new Response('Invalid secret', { status: 401 });
}

// we can use sameSite: 'strict' because we're running an embedded studio
// setCookie('__sanity_preview', randomBytes(16).toString('hex'), {
setCookie('__sanity_preview', 'true', {
path: '/',
secure: import.meta.env.PROD,
httpOnly: true,
sameSite: 'strict',
});
sendRedirect(redirectTo);
},
});
57 changes: 57 additions & 0 deletions apps/docs/dktp/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
properties:
configuration:
activeRevisionsMode: Single
ingress:
external: true
allowInsecure: false
targetPort: 3000
secrets:
- name: splunk-token
keyVaultUrl: https://dkt-nettsted-prod-kv.vault.azure.net/secrets/Splunk-Token
identity: System
template:
scale:
minReplicas: 1
maxReplicas: 3
containers:
- image: dktprodacr.azurecr.io/grunnmuren/docs:${IMAGE_TAG}
name: docs
env:
- name: SANITY_VIEWER_TOKEN
secretRef: ${todo}
resources:
cpu: 0.25
memory: 0.5Gi
probes:
- type: liveness
httpGet:
path: '/api/health'
port: 3000
initialDelaySeconds: 7
periodSeconds: 3
- type: readiness
httpGet:
path: '/api/health'
port: 3000
initialDelaySeconds: 10
periodSeconds: 3
volumeMounts:
- mountPath: /var/log/console-logs
volumeName: logs

- image: dktprodacr.azurecr.io/dktp/log-forwarder:latest
name: logs
env:
- name: SPLUNK_TOKEN
secretRef: splunk-token
- name: ENVIRONMENT
value: prod
resources:
cpu: 0.25
memory: 0.5Gi
volumeMounts:
- mountPath: /var/log/console-logs
volumeName: logs
volumes:
- name: logs
storageType: EmptyDir
21 changes: 12 additions & 9 deletions apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,21 @@
"@portabletext/react": "6.0.3",
"@react-aria/utils": "3.34.0",
"@sanity/asset-utils": "2.3.0",
"@sanity/assist": "5.0.4",
"@sanity/client": "7.16.0",
"@sanity/code-input": "7.0.9",
"@sanity/image-url": "2.0.3",
"@sanity/assist": "6.0.4",
"@sanity/client": "7.22.0",
"@sanity/code-input": "7.1.0",
"@sanity/image-url": "2.1.1",
"@sanity/preview-url-secret": "4.0.5",
"@sanity/react-loader": "^2.0.9",
"@sanity/table": "2.0.1",
"@sanity/vision": "5.13.0",
"@sanity/vision": "5.22.0",
"@sanity/visual-editing": "5.3.4",
"@tanstack/nitro-v2-vite-plugin": "1.154.9",
"@tanstack/react-router": "1.168.21",
"@tanstack/react-start": "1.167.39",
"@tanstack/react-router": "1.168.25",
"@tanstack/react-start": "1.167.50",
"@vitejs/plugin-react": "6.0.1",
"cva": "1.0.0-beta.4",
"groq": "5.13.0",
"groq": "5.22.0",
"pino-opentelemetry-transport": "3.0.0",
"prism-react-renderer": "2.4.1",
"react": "19.2.3",
Expand All @@ -50,7 +53,7 @@
"react-element-to-jsx-string": "17.0.1",
"react-live": "4.1.8",
"react-shiki": "0.9.1",
"sanity": "5.13.0",
"sanity": "5.22.0",
"styled-components": "6.4.0",
"use-debounce": "10.1.1"
},
Expand Down
17 changes: 17 additions & 0 deletions apps/docs/sanity.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { codeInput } from '@sanity/code-input';
import { table } from '@sanity/table';
import { visionTool } from '@sanity/vision';
import { defineConfig } from 'sanity';
import { defineDocuments, presentationTool } from 'sanity/presentation';
import { structureTool } from 'sanity/structure';

import { schemaTypes } from './studio/schema-types';
Expand Down Expand Up @@ -64,6 +65,22 @@ export default defineConfig({
codeInput(),
table(),
assist(),
presentationTool({
previewUrl: {
previewMode: {
enable: '/api/preview-mode/enable',
disable: '/api/preview-mode/disable',
},
},
resolve: {
mainDocuments: defineDocuments([
{
route: '/komponenter/:slug',
filter: `_type == "component" && slug.current == $slug`,
},
]),
},
}),
],
schema: {
types: schemaTypes,
Expand Down
20 changes: 20 additions & 0 deletions apps/docs/src/functions/sanity.loader.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createClient } from '@sanity/client';
import { loadQuery } from '@sanity/react-loader';

import { setServerClient } from '../lib/sanity.loader';

export const sanityLoaderServer = () => {
const client = createClient({
projectId: 'tq6w17ny',
dataset: 'grunnmuren',
apiVersion: '2026-04-27',
useCdn: true,
stega: {
enabled: true,
studioUrl: 'http://localhost:3333/studio',
},
});

setServerClient(client);
return loadQuery;
};
12 changes: 12 additions & 0 deletions apps/docs/src/lib/preview-middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createMiddleware } from '@tanstack/start';
import { getCookie } from 'vinxi/http';

export const previewMiddleware = createMiddleware().server(({ next }) => {
const isPreview = getCookie('__sanity_preview') === 'true';
console.log('middleware', { isPreview });
return next({
context: {
previewMode: isPreview,
},
});
});
Loading
Loading