diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3592883c5e7..85b94ab26d9 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,6 +3,9 @@ Brief description of what this PR does and why. Fixes #(issue) +## Product update (if relevant) +Who benefits, and what can they do now? Note rollout or plan restrictions, link the relevant Sim integration or docs guide, and include a real product demo or changelog production brief when available. + ## Type of Change - [ ] Bug fix - [ ] New feature diff --git a/.github/workflows/helm.yml b/.github/workflows/helm.yml index 3cce2b7ae35..1f61ba8c724 100644 --- a/.github/workflows/helm.yml +++ b/.github/workflows/helm.yml @@ -37,12 +37,17 @@ jobs: runs-on: &runner-2vcpu ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }} timeout-minutes: 15 steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 - with: - # ct diffs the chart against the PR base to decide whether the version - # was bumped, so a shallow clone would leave it nothing to compare. + - name: Checkout code (mirror) + if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork + uses: useblacksmith/checkout@25227e61ff9dafe400e22fa487b673eac4e4409a # v1.8.1 + with: &chart-checkout-options + # Chart version checks need full history to compare against the PR base. fetch-depth: 0 persist-credentials: false + - name: Checkout code + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + with: *chart-checkout-options - name: Set up Helm uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1 diff --git a/apps/sim/AGENTS.md b/apps/sim/AGENTS.md index 84feeac85ce..9da2a087205 100644 --- a/apps/sim/AGENTS.md +++ b/apps/sim/AGENTS.md @@ -21,7 +21,7 @@ For a common task, start from its skill (`.agents/skills//SKILL.md`): -# This is NOT the Next.js you know +## This is NOT the Next.js you know This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. diff --git a/apps/sim/app/(landing)/changelog/[slug]/page.tsx b/apps/sim/app/(landing)/changelog/[slug]/page.tsx new file mode 100644 index 00000000000..ec1ffb7f4bf --- /dev/null +++ b/apps/sim/app/(landing)/changelog/[slug]/page.tsx @@ -0,0 +1,36 @@ +import type { Metadata } from 'next' +import { notFound } from 'next/navigation' +import { CHANGELOG_SECTION, getAllEntryMeta, getEntryBySlug } from '@/lib/changelog' +import { buildPostGraphJsonLd, buildPostMetadata } from '@/lib/content/seo' +import { ChangelogArticle, ChangelogLayout } from '@/app/(landing)/changelog/components' +import { JsonLd } from '@/app/(landing)/components/json-ld' + +export const dynamicParams = false +export const revalidate = 3600 + +interface ChangelogEntryPageProps { + params: Promise<{ slug: string }> +} + +export async function generateStaticParams() { + return (await getAllEntryMeta()).map((entry) => ({ slug: entry.slug })) +} + +export async function generateMetadata({ params }: ChangelogEntryPageProps): Promise { + const { slug } = await params + const entry = await getEntryBySlug(slug) + return entry ? buildPostMetadata(entry) : {} +} + +export default async function ChangelogEntryPage({ params }: ChangelogEntryPageProps) { + const { slug } = await params + const entry = await getEntryBySlug(slug) + if (!entry) notFound() + + return ( + + + + + ) +} diff --git a/apps/sim/app/(landing)/changelog/archive/page.tsx b/apps/sim/app/(landing)/changelog/archive/page.tsx new file mode 100644 index 00000000000..33fa49c6b33 --- /dev/null +++ b/apps/sim/app/(landing)/changelog/archive/page.tsx @@ -0,0 +1,44 @@ +import { CHANGELOG_SECTION, getAllEntryMeta } from '@/lib/changelog' +import { buildCollectionPageJsonLd } from '@/lib/content/seo' +import { buildLandingMetadata } from '@/lib/landing/seo' +import { + ChangelogHeader, + ChangelogLayout, + ChangelogList, +} from '@/app/(landing)/changelog/components' +import { BackLink } from '@/app/(landing)/components/back-link' +import { JsonLd } from '@/app/(landing)/components/json-ld' + +export const revalidate = 3600 +export const metadata = buildLandingMetadata({ + title: 'Changelog Archive | Sim', + description: 'Browse product updates from Sim and find the full technical release history.', + path: '/changelog/archive', +}) + +export default async function ChangelogArchivePage() { + const entries = await getAllEntryMeta() + return ( + + + } + /> + + + Earlier releases and full technical history on GitHub + + + ) +} diff --git a/apps/sim/app/(landing)/changelog/changelog.tsx b/apps/sim/app/(landing)/changelog/changelog.tsx index 78842b7f5c2..ceac662a224 100644 --- a/apps/sim/app/(landing)/changelog/changelog.tsx +++ b/apps/sim/app/(landing)/changelog/changelog.tsx @@ -1,50 +1,41 @@ -import { createLogger } from '@sim/logger' -import { getErrorMessage } from '@sim/utils/errors' -import { ChangelogActions, ChangelogTimeline } from '@/app/(landing)/changelog/components' -import type { ChangelogEntry, GitHubRelease } from '@/app/(landing)/changelog/types' -import { mapReleases, releasesEndpoint } from '@/app/(landing)/changelog/utils' -import { ProseHero, ProseShell } from '@/app/(landing)/components/prose-page' - -const logger = createLogger('Changelog') - -/** - * Changelog page - reuses the shared prose primitives ({@link ProseShell} + - * {@link ProseHero}) so its headline and column match Terms and Privacy, then - * renders the GitHub-release timeline. The first page of releases is - * fetched here on the server at build/revalidate time; the {@link ChangelogTimeline} - * client leaf paginates the rest. Re-authored from the prior dark changelog onto - * the platform light tokens. - */ - -const LEAD = - 'Every new feature, improvement, and fix in Sim, the open-source AI workspace, with release notes straight from GitHub.' - -async function getInitialEntries(): Promise { - try { - // boundary-raw-fetch: external GitHub Releases API (cross-origin), not a same-origin contract - const res = await fetch(releasesEndpoint(1), { - headers: { Accept: 'application/vnd.github+json' }, - next: { revalidate: 3600 }, - }) - const releases = (await res.json()) as GitHubRelease[] - return mapReleases(releases ?? []) - } catch (error) { - logger.warn('Failed to load initial changelog releases from GitHub', { - error: getErrorMessage(error), - }) - return [] - } -} +import Link from 'next/link' +import { CHANGELOG_SECTION, getAllEntryMeta, LATEST_ENTRY_LIMIT } from '@/lib/changelog' +import { buildCollectionPageJsonLd } from '@/lib/content/seo' +import { + ChangelogActions, + ChangelogHeader, + ChangelogLayout, + ChangelogList, +} from '@/app/(landing)/changelog/components' +import { JsonLd } from '@/app/(landing)/components/json-ld' export default async function Changelog() { - const entries = await getInitialEntries() - + const metadata = await getAllEntryMeta() + const visible = metadata.slice(0, LATEST_ENTRY_LIMIT) + const firstOlderEntry = metadata[LATEST_ENTRY_LIMIT] return ( - - } /> -
- -
-
+ + + } /> + +
+ {firstOlderEntry ? ( + + Older updates + + ) : null} + + Earlier releases on GitHub + +
+
) } diff --git a/apps/sim/app/(landing)/changelog/components/changelog-actions/changelog-actions.tsx b/apps/sim/app/(landing)/changelog/components/changelog-actions/changelog-actions.tsx index 686696ff15b..aa8c349564d 100644 --- a/apps/sim/app/(landing)/changelog/components/changelog-actions/changelog-actions.tsx +++ b/apps/sim/app/(landing)/changelog/components/changelog-actions/changelog-actions.tsx @@ -9,20 +9,18 @@ import { GithubOutlineIcon } from '@/components/icons' * beneath the changelog headline. A small client leaf because `ChipLink` is a * Client Component and its `leftIcon` is a component reference that cannot cross * the server→client boundary as a prop (same pattern as the platform pill CTA). - * GitHub is the primary filled chip; Docs and RSS are the default pills. */ export function ChangelogActions() { return (
- View on GitHub + Technical releases +
+ +
+ {entry.draft ? Draft : null} + + {entry.release ? {entry.release.versions.join(' · ')} : null} + {updated ? : null} +
+

+ {entry.title} +

+
+
+

+ In this update +

+ +
+ + + ) +} diff --git a/apps/sim/app/(landing)/changelog/components/changelog-article/index.ts b/apps/sim/app/(landing)/changelog/components/changelog-article/index.ts new file mode 100644 index 00000000000..893299fad9c --- /dev/null +++ b/apps/sim/app/(landing)/changelog/components/changelog-article/index.ts @@ -0,0 +1 @@ +export { ChangelogArticle } from './changelog-article' diff --git a/apps/sim/app/(landing)/changelog/components/changelog-header/changelog-header.tsx b/apps/sim/app/(landing)/changelog/components/changelog-header/changelog-header.tsx new file mode 100644 index 00000000000..fa62a3675f3 --- /dev/null +++ b/apps/sim/app/(landing)/changelog/components/changelog-header/changelog-header.tsx @@ -0,0 +1,23 @@ +import type { ReactNode } from 'react' +import { cn } from '@sim/emcn' +import { LANDING_TYPE } from '@/app/(landing)/components/landing-layout' + +interface ChangelogHeaderProps { + title: string + lead?: string + actions?: ReactNode +} + +export function ChangelogHeader({ title, lead, actions }: ChangelogHeaderProps) { + return ( +
+

{title}

+ {lead ? ( +

+ {lead} +

+ ) : null} + {actions} +
+ ) +} diff --git a/apps/sim/app/(landing)/changelog/components/changelog-header/index.ts b/apps/sim/app/(landing)/changelog/components/changelog-header/index.ts new file mode 100644 index 00000000000..62956d444f2 --- /dev/null +++ b/apps/sim/app/(landing)/changelog/components/changelog-header/index.ts @@ -0,0 +1 @@ +export { ChangelogHeader } from './changelog-header' diff --git a/apps/sim/app/(landing)/changelog/components/changelog-image/changelog-image.tsx b/apps/sim/app/(landing)/changelog/components/changelog-image/changelog-image.tsx new file mode 100644 index 00000000000..018acaf8223 --- /dev/null +++ b/apps/sim/app/(landing)/changelog/components/changelog-image/changelog-image.tsx @@ -0,0 +1,41 @@ +import { cn, Lightbox } from '@sim/emcn' +import Image from 'next/image' +import { LANDING_STAGE_RADIUS } from '@/app/(landing)/components/landing-layout' + +interface ChangelogImageProps { + src: string + alt: string + width: number | `${number}` + height: number | `${number}` + caption?: string +} + +/** Product screenshots reserve their intrinsic size and use the site's image optimization. */ +export function ChangelogImage({ src, alt, width, height, caption }: ChangelogImageProps) { + return ( +
+ + + + {caption ? ( +
{caption}
+ ) : null} +
+ ) +} diff --git a/apps/sim/app/(landing)/changelog/components/changelog-image/index.ts b/apps/sim/app/(landing)/changelog/components/changelog-image/index.ts new file mode 100644 index 00000000000..a29106002e5 --- /dev/null +++ b/apps/sim/app/(landing)/changelog/components/changelog-image/index.ts @@ -0,0 +1 @@ +export { ChangelogImage } from './changelog-image' diff --git a/apps/sim/app/(landing)/changelog/components/changelog-layout/changelog-layout.tsx b/apps/sim/app/(landing)/changelog/components/changelog-layout/changelog-layout.tsx new file mode 100644 index 00000000000..a829958a008 --- /dev/null +++ b/apps/sim/app/(landing)/changelog/components/changelog-layout/changelog-layout.tsx @@ -0,0 +1,24 @@ +import type { ReactNode } from 'react' +import { cn } from '@sim/emcn' +import { + HOME_INSET, + LANDING_CONTENT_WIDTH, + LANDING_GUTTER, + LANDING_HERO_TOP_PADDING, +} from '@/app/(landing)/components/landing-layout' + +interface ChangelogLayoutProps { + children: ReactNode +} + +/** Matches the marketing page frame; updates inherit the shared navbar and footer. */ +export function ChangelogLayout({ children }: ChangelogLayoutProps) { + return ( +
+
{children}
+
+ ) +} diff --git a/apps/sim/app/(landing)/changelog/components/changelog-layout/index.ts b/apps/sim/app/(landing)/changelog/components/changelog-layout/index.ts new file mode 100644 index 00000000000..ea8c7b7f3e3 --- /dev/null +++ b/apps/sim/app/(landing)/changelog/components/changelog-layout/index.ts @@ -0,0 +1 @@ +export { ChangelogLayout } from './changelog-layout' diff --git a/apps/sim/app/(landing)/changelog/components/changelog-list/changelog-list.tsx b/apps/sim/app/(landing)/changelog/components/changelog-list/changelog-list.tsx new file mode 100644 index 00000000000..a7f84530946 --- /dev/null +++ b/apps/sim/app/(landing)/changelog/components/changelog-list/changelog-list.tsx @@ -0,0 +1,31 @@ +import type { ContentMeta } from '@/lib/content/schema' +import { ChangelogEntry } from '@/app/(landing)/changelog/components/changelog-list/components/changelog-entry' + +interface ChangelogListProps { + entries: ContentMeta[] + basePath?: string +} + +/** A chronological index keeps demos and detailed copy on each update's page. */ +export function ChangelogList({ entries, basePath }: ChangelogListProps) { + return ( +
+

+ Product updates +

+ {entries.length > 0 ? ( +
    + {entries.map((entry) => ( +
  1. + +
  2. + ))} +
+ ) : ( +

+ Product updates will appear here as they are published. +

+ )} +
+ ) +} diff --git a/apps/sim/app/(landing)/changelog/components/changelog-list/components/changelog-entry/changelog-entry.tsx b/apps/sim/app/(landing)/changelog/components/changelog-list/components/changelog-entry/changelog-entry.tsx new file mode 100644 index 00000000000..78b9a200ef7 --- /dev/null +++ b/apps/sim/app/(landing)/changelog/components/changelog-list/components/changelog-entry/changelog-entry.tsx @@ -0,0 +1,42 @@ +import { cn } from '@sim/emcn' +import { ArrowRight } from '@sim/emcn/icons' +import Link from 'next/link' +import { CHANGELOG_SECTION } from '@/lib/changelog/constants' +import type { ContentMeta } from '@/lib/content/schema' +import { formatPostDate } from '@/app/(landing)/components/content-utils' +import { HOME_TYPE } from '@/app/(landing)/components/landing-layout' + +interface ChangelogEntryProps { + entry: ContentMeta + basePath?: string +} + +export function ChangelogEntry({ + entry, + basePath = CHANGELOG_SECTION.basePath, +}: ChangelogEntryProps) { + return ( +
+ +
+ + {entry.draft ? Draft : null} +
+

+ {entry.title} +

+
+ ) +} diff --git a/apps/sim/app/(landing)/changelog/components/changelog-list/components/changelog-entry/index.ts b/apps/sim/app/(landing)/changelog/components/changelog-list/components/changelog-entry/index.ts new file mode 100644 index 00000000000..425eaf1e0ef --- /dev/null +++ b/apps/sim/app/(landing)/changelog/components/changelog-list/components/changelog-entry/index.ts @@ -0,0 +1 @@ +export { ChangelogEntry } from './changelog-entry' diff --git a/apps/sim/app/(landing)/changelog/components/changelog-list/index.ts b/apps/sim/app/(landing)/changelog/components/changelog-list/index.ts new file mode 100644 index 00000000000..09282933bff --- /dev/null +++ b/apps/sim/app/(landing)/changelog/components/changelog-list/index.ts @@ -0,0 +1 @@ +export { ChangelogList } from './changelog-list' diff --git a/apps/sim/app/(landing)/changelog/components/changelog-timeline/changelog-timeline.tsx b/apps/sim/app/(landing)/changelog/components/changelog-timeline/changelog-timeline.tsx deleted file mode 100644 index eb882c155fe..00000000000 --- a/apps/sim/app/(landing)/changelog/components/changelog-timeline/changelog-timeline.tsx +++ /dev/null @@ -1,220 +0,0 @@ -'use client' - -import { type ReactNode, useRef, useState } from 'react' -import { Streamdown } from 'streamdown' -import 'streamdown/styles.css' -import { Avatar, AvatarFallback, AvatarImage, Chip, cn } from '@sim/emcn' -import { formatDate } from '@sim/utils/formatting' -import type { ChangelogEntry, GitHubRelease } from '@/app/(landing)/changelog/types' -import { mapReleases, releasesEndpoint } from '@/app/(landing)/changelog/utils' - -/** - * The changelog timeline - the single client leaf of the changelog page. Renders - * each GitHub release as a `
` (an `

` version tag + contributor - * avatars + cleaned markdown via {@link Streamdown}) and paginates further pages - * from the GitHub Releases API on demand. Re-authored from the prior dark - * timeline onto the platform light tokens; the fetch, markdown cleaning, and - * load-more behavior are preserved. - */ - -interface ChangelogTimelineProps { - initialEntries: ChangelogEntry[] -} - -function stripContributors(body: string): string { - let output = body - output = output.replace( - /(^|\n)#{1,6}\s*Contributors\s*\n[\s\S]*?(?=\n\s*\n|\n#{1,6}\s|$)/gi, - '\n' - ) - output = output.replace( - /(^|\n)\s*(?:\*\*|__)?\s*Contributors\s*(?:\*\*|__)?\s*:?\s*\n[\s\S]*?(?=\n\s*\n|\n#{1,6}\s|$)/gi, - '\n' - ) - output = output.replace( - /(^|\n)[-*+]\s*(?:@[A-Za-z0-9-]+(?:\s*,\s*|\s+))+@[A-Za-z0-9-]+\s*(?=\n)/g, - '\n' - ) - output = output.replace( - /(^|\n)\s*(?:@[A-Za-z0-9-]+(?:\s*,\s*|\s+))+@[A-Za-z0-9-]+\s*(?=\n)/g, - '\n' - ) - return output -} - -function stripPrReferences(body: string): string { - return body.replace(/\s*\(\s*\[#\d+\]\([^)]*\)\s*\)/g, '').replace(/\s*\(\s*#\d+\s*\)/g, '') -} - -function cleanMarkdown(body: string): string { - return stripPrReferences(stripContributors(body)) -} - -function isContributorsLabel(children: ReactNode): boolean { - return /^\s*contributors\s*:?\s*$/i.test(String(children)) -} - -export function ChangelogTimeline({ initialEntries }: ChangelogTimelineProps) { - const [entries, setEntries] = useState(initialEntries) - const [loading, setLoading] = useState(false) - const [done, setDone] = useState(false) - const pageRef = useRef(1) - - const loadMore = async () => { - if (loading || done) return - setLoading(true) - try { - const nextPage = pageRef.current + 1 - // boundary-raw-fetch: external GitHub Releases API (cross-origin), not a same-origin contract - const res = await fetch(releasesEndpoint(nextPage), { - headers: { Accept: 'application/vnd.github+json' }, - }) - const releases = (await res.json()) as GitHubRelease[] - const mapped = mapReleases(releases ?? []) - - if (mapped.length === 0) { - setDone(true) - } else { - setEntries((prev) => [...prev, ...mapped]) - pageRef.current = nextPage - } - } catch { - setDone(true) - } finally { - setLoading(false) - } - } - - return ( -
- {entries.map((entry) => { - const headingId = `release-${entry.tag}-heading` - return ( -
-
-
-

- {entry.tag} -

- {entry.contributors.length > 0 ? ( -
- {entry.contributors.slice(0, 5).map((contributor) => ( - - - - {contributor.slice(0, 2).toUpperCase()} - - - ))} - {entry.contributors.length > 5 ? ( -
- +{entry.contributors.length - 5} -
- ) : null} -
- ) : null} -
- - {formatDate(new Date(entry.date))} - -
- -
- ) - })} - - {!done ? ( -
- - {loading ? 'Loading…' : 'Show more'} - -
- ) : null} -
- ) -} diff --git a/apps/sim/app/(landing)/changelog/components/changelog-timeline/index.ts b/apps/sim/app/(landing)/changelog/components/changelog-timeline/index.ts deleted file mode 100644 index f7ac9baa67a..00000000000 --- a/apps/sim/app/(landing)/changelog/components/changelog-timeline/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ChangelogTimeline } from './changelog-timeline' diff --git a/apps/sim/app/(landing)/changelog/components/changelog-video/changelog-video.tsx b/apps/sim/app/(landing)/changelog/components/changelog-video/changelog-video.tsx new file mode 100644 index 00000000000..5d6c56ea001 --- /dev/null +++ b/apps/sim/app/(landing)/changelog/components/changelog-video/changelog-video.tsx @@ -0,0 +1,38 @@ +import { cn } from '@sim/emcn' +import { isChangelogMediaSource } from '@/lib/changelog/media' +import { LANDING_STAGE_RADIUS } from '@/app/(landing)/components/landing-layout' + +interface ChangelogVideoProps { + src: string + poster: string + /** Accessible description for the player; not repeated below the video. */ + caption: string + captionsSrc?: string +} + +/** Product recordings load on demand and retain a readable description without playback. */ +export function ChangelogVideo({ src, poster, caption, captionsSrc }: ChangelogVideoProps) { + if (!isChangelogMediaSource(src) || (captionsSrc && !isChangelogMediaSource(captionsSrc))) { + throw new Error('Changelog recordings must use same-origin paths or the approved media CDN') + } + return ( +
+ +
+ ) +} diff --git a/apps/sim/app/(landing)/changelog/components/changelog-video/index.ts b/apps/sim/app/(landing)/changelog/components/changelog-video/index.ts new file mode 100644 index 00000000000..e766a87170d --- /dev/null +++ b/apps/sim/app/(landing)/changelog/components/changelog-video/index.ts @@ -0,0 +1 @@ +export { ChangelogVideo } from './changelog-video' diff --git a/apps/sim/app/(landing)/changelog/components/index.ts b/apps/sim/app/(landing)/changelog/components/index.ts index 5af19189c40..3e41a2dd2d5 100644 --- a/apps/sim/app/(landing)/changelog/components/index.ts +++ b/apps/sim/app/(landing)/changelog/components/index.ts @@ -1,2 +1,7 @@ export { ChangelogActions } from './changelog-actions' -export { ChangelogTimeline } from './changelog-timeline' +export { ChangelogArticle } from './changelog-article' +export { ChangelogHeader } from './changelog-header' +export { ChangelogImage } from './changelog-image' +export { ChangelogLayout } from './changelog-layout' +export { ChangelogList } from './changelog-list' +export { ChangelogVideo } from './changelog-video' diff --git a/apps/sim/app/(landing)/changelog/page.tsx b/apps/sim/app/(landing)/changelog/page.tsx index 46180cfc7c0..597ea9a1e49 100644 --- a/apps/sim/app/(landing)/changelog/page.tsx +++ b/apps/sim/app/(landing)/changelog/page.tsx @@ -1,15 +1,12 @@ +import { CHANGELOG_SECTION } from '@/lib/changelog' import { buildLandingMetadata } from '@/lib/landing/seo' import Changelog from '@/app/(landing)/changelog/changelog' export const revalidate = 3600 -const TITLE = 'Changelog | Sim, the AI Workspace' -const DESCRIPTION = - 'Every new feature, improvement, and fix in Sim, the open-source AI workspace, with release notes straight from GitHub.' - export const metadata = buildLandingMetadata({ - title: TITLE, - description: DESCRIPTION, + title: 'Changelog | Sim, the AI Workspace', + description: CHANGELOG_SECTION.description, path: '/changelog', }) diff --git a/apps/sim/app/(landing)/changelog/preview/[slug]/page.tsx b/apps/sim/app/(landing)/changelog/preview/[slug]/page.tsx new file mode 100644 index 00000000000..69caf40d571 --- /dev/null +++ b/apps/sim/app/(landing)/changelog/preview/[slug]/page.tsx @@ -0,0 +1,32 @@ +import type { Metadata } from 'next' +import { notFound } from 'next/navigation' +import { getEntryPreview } from '@/lib/changelog' +import { ChangelogArticle, ChangelogLayout } from '@/app/(landing)/changelog/components' + +export const metadata: Metadata = { + title: { absolute: 'Changelog preview | Sim' }, + robots: { index: false, follow: false }, +} + +interface ChangelogDraftPageProps { + params: Promise<{ slug: string }> +} + +export default async function ChangelogDraftPage({ params }: ChangelogDraftPageProps) { + if (process.env.NODE_ENV !== 'development') notFound() + const { slug } = await params + const entry = await getEntryPreview(slug) + if (!entry) notFound() + return ( + +

+ Local editorial preview. Verify the feature, availability, and media before publishing. +

+ +
+ ) +} diff --git a/apps/sim/app/(landing)/changelog/preview/page.tsx b/apps/sim/app/(landing)/changelog/preview/page.tsx new file mode 100644 index 00000000000..cc661be364a --- /dev/null +++ b/apps/sim/app/(landing)/changelog/preview/page.tsx @@ -0,0 +1,27 @@ +import type { Metadata } from 'next' +import { notFound } from 'next/navigation' +import { getAllEntryPreviews } from '@/lib/changelog' +import { + ChangelogHeader, + ChangelogLayout, + ChangelogList, +} from '@/app/(landing)/changelog/components' + +export const metadata: Metadata = { + title: { absolute: 'Changelog preview | Sim' }, + robots: { index: false, follow: false }, +} + +export default async function ChangelogPreviewPage() { + if (process.env.NODE_ENV !== 'development') notFound() + const entries = await getAllEntryPreviews() + return ( + + + + + ) +} diff --git a/apps/sim/app/(landing)/changelog/types.ts b/apps/sim/app/(landing)/changelog/types.ts deleted file mode 100644 index c3c57754672..00000000000 --- a/apps/sim/app/(landing)/changelog/types.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Changelog types shared between the server page (initial fetch) and the client - * timeline (load-more). {@link GitHubRelease} is the minimal shape we read from - * the GitHub Releases API; {@link ChangelogEntry} is the normalized entry the UI - * renders. - */ - -/** The minimal subset of a GitHub Releases API item that the changelog reads. */ -export interface GitHubRelease { - tag_name: string - name: string | null - body: string | null - published_at: string - html_url: string - prerelease: boolean -} - -/** A normalized changelog entry rendered in the timeline. */ -export interface ChangelogEntry { - tag: string - title: string - content: string - date: string - url: string - contributors: string[] -} diff --git a/apps/sim/app/(landing)/changelog/utils.ts b/apps/sim/app/(landing)/changelog/utils.ts deleted file mode 100644 index 4abdc06a042..00000000000 --- a/apps/sim/app/(landing)/changelog/utils.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { ChangelogEntry, GitHubRelease } from '@/app/(landing)/changelog/types' - -/** - * Changelog helpers shared by the server page (initial page) and the client - * timeline (subsequent pages), so the GitHub-release → entry mapping is defined - * once and both surfaces stay in sync. - */ - -/** How many releases to request per GitHub API page. */ -export const RELEASES_PER_PAGE = 10 - -/** Builds the GitHub Releases endpoint for a given 1-based page. */ -export function releasesEndpoint(page: number): string { - return `https://api.github.com/repos/simstudioai/sim/releases?per_page=${RELEASES_PER_PAGE}&page=${page}` -} - -/** Removes literal ` ` artifacts from release bodies. */ -export function sanitizeContent(body: string): string { - return body.replace(/ /g, '') -} - -/** Extracts unique `@handle` GitHub mentions from a release body. */ -export function extractMentions(body: string): string[] { - const matches = body.match(/@([A-Za-z0-9-]+)/g) ?? [] - return Array.from(new Set(matches.map((mention) => mention.slice(1)))) -} - -/** Maps non-prerelease GitHub releases to normalized {@link ChangelogEntry} items. */ -export function mapReleases(releases: GitHubRelease[]): ChangelogEntry[] { - return releases.reduce((acc, release) => { - if (release.prerelease) { - return acc - } - - const body = String(release.body ?? '') - acc.push({ - tag: release.tag_name, - title: release.name || release.tag_name, - content: sanitizeContent(body), - date: release.published_at, - url: release.html_url, - contributors: extractMentions(body), - }) - return acc - }, []) -} diff --git a/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-preview/components/changelog-menu-preview/changelog-menu-preview.tsx b/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-preview/components/changelog-menu-preview/changelog-menu-preview.tsx index 48001f86b35..c11cc4a0f71 100644 --- a/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-preview/components/changelog-menu-preview/changelog-menu-preview.tsx +++ b/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-preview/components/changelog-menu-preview/changelog-menu-preview.tsx @@ -1,46 +1,43 @@ +import { cn } from '@sim/emcn' import { Calendar } from '@sim/emcn/icons' +import Image from 'next/image' +import { LANDING_STAGE_RADIUS } from '@/app/(landing)/components/landing-layout' import { MenuPreviewFrame } from '@/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-preview/components/menu-preview-frame' import { MenuPreviewHeader } from '@/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-preview/components/menu-preview-header/menu-preview-header' -/** Release dates and summaries come from the checked-in release announcements. */ -const RELEASES = [ - { - date: 'Mar 17', - year: '2026', - title: 'Your agents, in one workspace', - description: 'One workspace for your agents, data, and tools.', - }, - { - date: 'Jan 22', - year: '2026', - title: 'Copilot & MCP deployment', - description: 'Build with context. Deploy any workflow as a tool.', - }, -] as const - -/** An open release timeline with quiet separators and content extending past the cropped window. */ +/** An illustrative crop of the changelog's featured update and story cards. */ export function ChangelogMenuPreview() { return (
-
-
- {RELEASES.map(({ date, year, title, description }) => ( -
-
-
{date}
-
{year}
-
- -
-
{title}
-

- {description} -

+
+
+ +
+
+ Bring Power BI data into your agents
+

+ Query models, inspect reports, and request refreshes. +

+
+
+
+
+ Review changes before syncing a workspace fork
- ))} +
Compare workflow deployments
+
diff --git a/apps/sim/app/(landing)/components/prose-page/index.ts b/apps/sim/app/(landing)/components/prose-page/index.ts index 45977944113..0a33e3ddf70 100644 --- a/apps/sim/app/(landing)/components/prose-page/index.ts +++ b/apps/sim/app/(landing)/components/prose-page/index.ts @@ -1,6 +1,4 @@ -export { ProseHero } from './components/prose-hero' export { ProseLink } from './components/prose-link' -export { ProseShell } from './components/prose-shell' export { PROSE_SPACING, PROSE_TYPE } from './constants' export { ProsePage } from './prose-page' export type { LegalBlock, LegalPageConfig, LegalSection } from './types' diff --git a/apps/sim/app/changelog.xml/route.ts b/apps/sim/app/changelog.xml/route.ts index 9aee139447d..a9627fe37f1 100644 --- a/apps/sim/app/changelog.xml/route.ts +++ b/apps/sim/app/changelog.xml/route.ts @@ -1,21 +1,13 @@ import { NextResponse } from 'next/server' +import { getAllEntryMeta } from '@/lib/changelog' import { SITE_URL } from '@/lib/core/utils/urls' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -export const dynamic = 'force-static' -export const revalidate = 3600 +/** Request-scoped logging needs dynamic handling; successful feeds are cached by the CDN. */ +export const dynamic = 'force-dynamic' -interface Release { - id: number - tag_name: string - name: string - body: string - html_url: string - published_at: string - prerelease: boolean -} - -function escapeXml(str: string) { - return str +function escapeXml(value: string): string { + return value .replace(/&/g, '&') .replace(//g, '>') @@ -23,47 +15,41 @@ function escapeXml(str: string) { .replace(/'/g, ''') } -export async function GET() { - try { - const res = await fetch('https://api.github.com/repos/simstudioai/sim/releases', { - headers: { Accept: 'application/vnd.github+json' }, - next: { revalidate }, - }) - const releases: Release[] = await res.json() - const items = (releases || []) - .filter((r) => !r.prerelease) +/** RSS is a public XML protocol; its summaries share the website's published content source. */ +export const GET = withRouteHandler( + async () => { + const entries = (await getAllEntryMeta()).slice(0, 50) + const items = entries .map( - (r) => ` - - ${escapeXml(r.name || r.tag_name)} - ${r.html_url} - ${r.html_url} - ${new Date(r.published_at).toUTCString()} - - - ` + (entry) => ` + + ${escapeXml(entry.title)} + ${escapeXml(entry.canonical)} + ${escapeXml(entry.release?.url ?? entry.canonical)} + ${new Date(entry.date).toUTCString()} + ${escapeXml(entry.description)} + ` ) .join('') - - const xml = ` - - - Sim Changelog - ${SITE_URL}/changelog - Latest changes, fixes and updates in Sim. - en-us - ${items} - - ` - + const xml = ` + + Sim Changelog + ${SITE_URL}/changelog + New features, improvements, and fixes in Sim. + en-us${items} +` return new NextResponse(xml, { - status: 200, headers: { 'Content-Type': 'application/rss+xml; charset=utf-8', - 'Cache-Control': `public, s-maxage=${revalidate}, stale-while-revalidate=${revalidate}`, + 'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=3600', }, }) - } catch { - return new NextResponse('Service Unavailable', { status: 503 }) + }, + { + unhandledErrorResponse: () => + new NextResponse('Service Unavailable', { + status: 503, + headers: { 'Cache-Control': 'no-store', 'Retry-After': '60' }, + }), } -} +) diff --git a/apps/sim/app/sitemap.ts b/apps/sim/app/sitemap.ts index cc2871c811c..74dd91e119a 100644 --- a/apps/sim/app/sitemap.ts +++ b/apps/sim/app/sitemap.ts @@ -1,5 +1,6 @@ import type { MetadataRoute } from 'next' import { getAllPostMeta as getAllBlogPostMeta } from '@/lib/blog/registry' +import { getAllEntryMeta } from '@/lib/changelog' import type { ContentMeta } from '@/lib/content/schema' import { latestModified } from '@/lib/content/utils' import { SITE_URL } from '@/lib/core/utils/urls' @@ -42,10 +43,11 @@ function buildAuthorPages(posts: ContentMeta[], basePath: string): MetadataRoute */ export default async function sitemap(): Promise { const baseUrl = SITE_URL - const [posts, libraryPosts, customerStories] = await Promise.all([ + const [posts, libraryPosts, customerStories, changelogEntries] = await Promise.all([ getAllBlogPostMeta(), getAllLibraryPostMeta(), getAllCustomerStoryMeta(), + getAllEntryMeta(), ]) const latestPostDateValue = latestModified(posts) @@ -131,6 +133,11 @@ export default async function sitemap(): Promise { }, { url: `${baseUrl}/changelog`, + lastModified: latestModified(changelogEntries), + }, + { + url: `${baseUrl}/changelog/archive`, + lastModified: latestModified(changelogEntries), }, { url: `${baseUrl}/integrations`, @@ -222,6 +229,10 @@ export default async function sitemap(): Promise { const pages: MetadataRoute.Sitemap = [ ...staticPages, + ...changelogEntries.map((entry) => ({ + url: entry.canonical, + lastModified: new Date(entry.updated ?? entry.date), + })), ...blogPages, ...authorPages, ...libraryPages, diff --git a/apps/sim/content/changelog/README.md b/apps/sim/content/changelog/README.md new file mode 100644 index 00000000000..f7a4cfb671d --- /dev/null +++ b/apps/sim/content/changelog/README.md @@ -0,0 +1,141 @@ +# Publishing a Sim product update + +Create `content/changelog//index.mdx`. The slugs `archive` and `preview` are reserved. The website, archive, individual pages, sitemap, and RSS all read published entries from this directory through the shared content registry. + +The index shows up to 12 updates as dates and linked titles, without repeated summaries, thumbnails, or card borders. When more exist, an Older updates link opens the archive at the first omitted entry. The archive links to every published update. Both lists read metadata only; the full article and its media are loaded on the individual update page. Keep the concise description for search, sharing, and RSS. The reviewed poster supplies the share image; it does not need to repeat on the index. Article headers show the title and release date without a personal byline or another summary. The page frame, typography, media radius, and row spacing use Sim's shared landing styles. + +Write for a team using Sim: what changed, what they can do with it, and where to find it. Group related PRs into one product update. Keep maintenance, refactors, and routine content edits in the technical GitHub release history. + +The [editorial and media standard](./editorial-standard.md) defines story selection, concise copy, demo framing, poster selection, and the final quality review. Apply it to both the drafting workflow and hand-edited entries. + +## What earns an entry + +A public update needs one clear benefit, verified availability, a useful next step, and at least one reviewed feature image or video in the article. The cover alone is not enough. A reader should understand what they can now do and see evidence of that capability. A smaller improvement can join a related edition; it does not need its own announcement because a technical version shipped. + +Review candidates weekly and publish when a story meets that bar. Skip an edition if there is nothing meaningful to announce. Use a separate entry for a substantial launch. Aim for one lead story and a short selection of related improvements or fixes, rather than reproducing every commit. The GitHub release link carries the complete technical history. + +Always make consequential breaking changes, migrations, deprecations, and changes requiring action easy to find, including the affected version, availability, and next step. For a change with no useful product screen, a clearly labeled, accurate explanatory diagram can serve as the image. Do not delay an urgent notice to produce a polished video. + +The initial deployment-comparison, fork-comparison, and Power BI stories use current captures from the running app with sample data. Their production records identify the source changes, capture environment, and demonstration limits. The separate reliability candidate remains a draft until it has feature-specific evidence and media. Do not remove `draft: true` just to populate the page. + +## Production workflow + +Each new update has one folder containing its public `index.mdx` and a non-rendered `production.md` brief. The registry only reads `index.mdx`; the brief and announcement drafts are not website content. Start from the [deployment comparison brief](./compare-workflow-deployments/production.md). Assign one release editor and one feature owner in the PR; both must complete their review before publication. + +| Step | Owner | Output | +| --- | --- | --- | +| Gather evidence | Feature owner | Merged PRs, release version, deployment and permission availability, and a working example | +| Choose the story | Editor | One headline, the situation it helps with, and related improvements worth including | +| Prepare drafts | Editor, assisted by Codex | `index.mdx` with `draft: true`, demo shot list, and social/email copy in `production.md` | +| Produce media | Feature owner | Real product recording, poster, and captions for speech | +| Review | Feature owner and editor | Confirm the actual feature, rollout, and media; verify links and approve consistent copy | +| Deploy | Site maintainer | Merge the reviewed entry with `draft: false`; website, detail page, archive, sitemap, and RSS update together | +| Distribute | Editor | Send the reviewed announcement after the public URL and demo work | + +Codex can read PRs and diffs, group related changes, and produce copy and shot lists from that evidence. Keep commit subjects as research inputs. The editor checks the benefit, feature availability, restrictions, and the demonstration. A GitHub release and a Cloud deployment are separate events. Do not draft a launch from an unverified release title alone. + +For a new edition, `release.versions` can list several technical versions. When there is no single corresponding GitHub release, omit `release.url`; the editorial entry then has its own RSS identity. + +Open `/changelog/preview` while running `next dev` to review drafts with the same list, typography, and article components as the public site. `/changelog/preview/` shows one draft. These routes return 404 outside development, emit `noindex`, and are excluded from the sitemap and RSS. Public routes continue to exclude drafts even during development. The preview rereads metadata after edits, so there is no need to temporarily publish a draft. + +## Contextual links + +Give readers the next useful step in the sentence that introduces a capability. Link an integration's name to its actual `/integrations/` page and link its setup instructions to the corresponding `docs.sim.ai` guide. Catalog slugs can differ from tool IDs: You.com is `/integrations/you-com`, while its guide is `/integrations/youcom` on the docs domain. + +Use the guide for the exact surface being announced. Live Search connectors have their own `/search/` guides; an ordinary workflow integration page can describe a different connection and permission model. Feature announcements should link to the feature guide, and API changes should link to the current generated operation page. Related Sim product pages and explanatory blog posts are useful when they help a reader use or understand the update. + +For a model update, link the specific `/models//` page, or the provider catalog when discussing several models. Use the actual generated route, not a slug guessed from the model's display name. Keep the workflow setup link alongside it when useful. + +Choose a few relevant links instead of repeating every destination in a separate resource list. Keep link text descriptive. The content audit checks integration and model destinations against their generated catalogs and docs destinations against the MDX source and generated OpenAPI pages. Use current canonical URLs, including the operation ID's exact case. Confirm the linked page's instructions and any deep anchor in the deployment preview; a source file alone does not establish rollout or anchor correctness. + +## Search and answer-engine visibility + +Keep each summary independently useful: name Sim and the feature, explain the new capability, and state material availability limits in the article. Put setup steps and demonstration explanations in server-rendered text alongside the media. Do not leave important facts only inside a screenshot or video. + +The index and archive use real links, semantic lists, and CollectionPage/ItemList data matching the visible entries. Articles keep their own canonical URLs, BlogPosting data, authorship metadata, original publication dates, and substantive correction dates. Public articles appear in the sitemap and RSS; previews stay noindex and drafts stay out of public routes. Preserve these properties when changing the layout. If the archive eventually needs pagination, use server-rendered pages with real next/previous links and a self-canonical URL for each page; do not make older entries accessible only through a JavaScript button. + +These are standard search foundations, not a guarantee of indexing or AI citations. [Google's AI search guidance](https://developers.google.com/search/docs/appearance/ai-features) calls for crawlable, useful text, relevant media, internal links, and structured data matching the visible content; it does not require special AI-only markup. Avoid hidden keyword blocks, invented FAQs, and schema for information that the page does not show. + +After deployment, verify a representative article and the archive with Search Console URL Inspection, confirm the submitted sitemap is processed, and check Bing Webmaster Tools for crawl or indexing failures. During the monthly review, monitor indexed pages, search queries, and referrals, alongside link and media health. Local HTTP checks establish crawlability; they cannot prove that an external engine has indexed the live site. + +## Artifact delivery + +- Record one complete action and its visible result with sample data in a dedicated demo workspace. Verify the feature and permissions in that environment first. Use the product's normal theme and typography. Keep the cursor and text readable; crop around the relevant controls while preserving enough context to understand the action. +- Export an H.264 MP4 at a readable resolution, usually 1280×720 or 1920×1080. Keep it about 20–45 seconds and optimize it for web playback with `+faststart`. Prefer a focused demonstration with native playback controls over a decorative loop. Check the final crop on a 390px-wide screen. +- Take screenshots and the poster from the reviewed product capture. Choose a frame that shows the announced result, with a legible 16:9 crop for the story card. A separate 1200×630 share image can use the same frame with enough safe area for cropping. Keep the headline and benefit in the page's HTML rather than baking them into the image. Compress local JPEG, WebP, or PNG assets and retain actual screenshot dimensions in MDX. +- Use immutable, versioned media filenames, such as `compare-workflow-deployments-v1.mp4`. Upload recordings and spoken captions under `changelog/` in the existing public Academy asset store: `https://nnjgp7vypgx4myuq.public.blob.vercel-storage.com`. Put the returned public URLs in the entry. The app CSP and content audit explicitly allow this origin for recordings and captions. Small optimized walkthroughs under 1 MiB can live in `public/changelog/` and deploy atomically with their entry. Use the CDN for larger recordings. The component does not upload or proxy videos. +- Serve MP4s as `video/mp4` and spoken captions as `text/vtt`. The existing CDN supports HTTP Range requests, cross-origin playback, and caching. Use a new filename for every revision and verify those response headers on the uploaded asset. Do not overwrite an existing recording URL. +- In the deployment preview, check playback, seeking, captions, poster loading, and mobile readability. A silent recording uses the component's muted default. Spoken recordings need a WebVTT file. +- Generate social and email drafts from the reviewed product story: capability, practical benefit, and one link to the canonical entry. Use the same claims and restrictions everywhere. Keep distribution manual until the format and review process are established. + +AI can prepare storyboards, copy, captions, and clearly labeled conceptual diagrams. Screenshots and feature recordings must come from the real product. Do not generate fictional controls, results, performance numbers, or before-and-after evidence. Label a walkthrough assembled from still captures as a step-by-step walkthrough; do not describe it as a continuous recording. Keep its UI frames unmodified and record the assembly method in the production brief. + +Optional local encoding, once a real recording exists: + +```bash +ffmpeg -i recording.mov -vf "scale=1280:-2" -c:v libx264 -crf 23 -preset medium -pix_fmt yuv420p -c:a aac -movflags +faststart compare-workflow-deployments-v1.mp4 +ffmpeg -ss 25 -i compare-workflow-deployments-v1.mp4 -frames:v 1 -q:v 2 compare-workflow-deployments-poster.jpg +``` + +Choose the poster timestamp after reviewing the exported recording; `25` is an example, not a required frame. Check the compressed result rather than only the original recording. + +## Entry template + +```mdx +--- +slug: your-update +title: 'A concrete product headline' +description: 'Explain the new capability and the situation where it helps a team using Sim.' +date: '2026-10-07T18:00:00Z' +authors: [waleed] +tags: [Workflows] +ogImage: /changelog/your-update-poster.jpg +ogAlt: 'Describe the actual product view' +draft: true +technical: false +release: + versions: [v0.9.14] + url: https://github.com/simstudioai/sim/releases/tag/v0.9.14 +--- + + + +Explain where to find the feature and give one concrete example. + +### Improvements + +- Describe a noticeable improvement. + +### Fixes + +- Describe the situation that now works correctly. +``` + +## Review and media + +1. Check the source PRs and verify availability in the deployed product. A merge or GitHub release does not prove Cloud availability. Mention Enterprise restrictions and the minimum self-hosted version when relevant. +2. Use a dedicated demo workspace. Record one action from start to result in roughly 20–45 seconds, with readable text and sample data. +3. Use compressed MP4s on the approved public CDN, plus a local poster image, or same-origin MP4s under 1 MiB. Do not commit large recordings. Add WebVTT captions on the same origin for spoken recordings; omit `captionsSrc` for silent recordings. A different media provider needs an explicit CSP and content-audit change before use. +4. Use `` for a screenshot, supplying its actual dimensions as literal attributes to reserve space during loading. The MDX compiler strips JavaScript expressions such as `width={1200}`. Describe what it actually shows; do not use an older screenshot as proof of a new control. +5. Use `###` for sections inside an entry: the article owns the H1 and its details section owns an accessible H2. +6. Keep `draft: true` until the text, availability, media, and links are reviewed. Drafts are excluded from the public page, direct entry routes, archive, sitemap, and RSS. Use the development preview to review them without changing publication status. +7. Publish by removing the draft flag or setting it to false, then deploy the site. Keep the slug and original publication date stable. Add `updated` only for a substantive correction. + +Before merging, run `bun run check:library-content --slug changelog/` from the repository root. The shared content audit checks frontmatter, authors, folder slugs, reserved routes, images, MDX syntax, internal links, changelog media attributes, and the existence of local recordings and caption files. Published entries also require body media and descriptive cover alt text; future publication dates, correction dates before publication, and duplicate RSS identities fail the audit. `bun run check:audits` runs it in CI. It cannot determine whether an image proves a feature or confirm Cloud rollout, codec quality, or CDN availability; the feature owner and editor verify those in the brief and preview. + +For a migrated historical release, use its original publication timestamp and GitHub release URL. RSS retains that URL as its identifier to avoid announcing the same release twice. For a new editorial edition covering multiple releases, omit `release.url` and link to the technical releases in the body, so its RSS identity is its unique canonical URL. + +## Maintenance + +- The [Sim workflow maintenance guide](./maintenance-workflow.md) describes collection, the editorial queue, draft preparation, media production, and monitoring. The operational runbook and workflow exports in the editorial workspace record its live configuration and acceptance results. Keep this publishing guide and the editorial standard as the source of content and media rules. +- The release editor reviews candidates weekly and checks published links and hosted media monthly. Refresh destinations when docs move and verify deep anchors manually; the audit catches missing canonical source pages during every PR. +- Keep the original slug, publication date, and RSS identity stable. Use `updated` only for a substantive correction. A future date does not schedule publication; leave the entry in draft until the intended deployment. +- If availability changes after publication, update the existing entry with its current status and a clear next step. Preserve its URL rather than deleting it or reverting it to a draft. If a slug must change, add a redirect before moving it. +- Keep historical media versioned. Record the capture date, environment, demonstrated steps, and final asset URLs in the brief. Never overwrite an existing recording. Recheck playback, seeking, captions, and mobile readability after any replacement. +- RSS includes the 50 latest published entries, the homepage shows 12, and the archive retains the published history. Those collections load metadata and optimized covers; full articles and recordings load on their detail pages. +- Draft preparation can be assisted by Codex. Publication and distribution remain deliberate editorial actions; there is no automatic commit-to-changelog or auto-send job. diff --git a/apps/sim/content/changelog/compare-workflow-deployments/index.mdx b/apps/sim/content/changelog/compare-workflow-deployments/index.mdx new file mode 100644 index 00000000000..825bdc266b6 --- /dev/null +++ b/apps/sim/content/changelog/compare-workflow-deployments/index.mdx @@ -0,0 +1,27 @@ +--- +slug: compare-workflow-deployments +title: 'Compare workflow versions before you deploy' +description: 'See prompt edits, added steps, and changed connections in Sim. Compare two deployments or check your draft against the live workflow.' +date: '2026-10-02T06:55:11Z' +authors: [waleed] +tags: [Workflows] +ogImage: /changelog/compare-workflow-deployments-v1.jpg +ogAlt: 'Sim compares two workflow versions, highlighting a prompt edit and an added Slack step' +technical: false +draft: false +release: + versions: [v0.9.10] + url: https://github.com/simstudioai/sim/releases/tag/v0.9.10 +--- + + + +Open deployment history and choose **Compare** from a version's menu. Select a changed block to inspect its settings alongside the workflow. For a draft, choose **View changes** to compare it with the live version before redeploying. + +Use it to review a support agent's new instructions and the [Slack](/integrations/slack) step added to send its digest. The comparison is read-only and leaves your draft intact. + +Included in **v0.9.10**. See the [deployment guide](https://docs.sim.ai/workflows/deployment) for setup, or [compare versions through the API](https://docs.sim.ai/api-reference/workflows/compareWorkflowVersionsV2). diff --git a/apps/sim/content/changelog/compare-workflow-deployments/production.md b/apps/sim/content/changelog/compare-workflow-deployments/production.md new file mode 100644 index 00000000000..522d8eae2eb --- /dev/null +++ b/apps/sim/content/changelog/compare-workflow-deployments/production.md @@ -0,0 +1,42 @@ +# Deployment comparison: production record + +Status: prepared for publication review. Only `index.mdx` is rendered publicly. + +## Evidence + +- [PR #8455](https://github.com/simstudioai/sim/pull/8455), commit `4436f825cf`, included in [v0.9.10](https://github.com/simstudioai/sim/releases/tag/v0.9.10). +- Checked the implementation and the actual Compare flow in a local self-hosted app on October 7, 2026, based on staging commit `684b228623`. +- Public copy states the released version. This local verification does not establish every Cloud organization's rollout. +- Links: [deployment guide](https://docs.sim.ai/workflows/deployment), [comparison API](https://docs.sim.ai/api-reference/workflows/compareWorkflowVersionsV2), and the Slack integration catalog. + +## Media provenance + +A dedicated local workspace contains a synthetic support-ticket workflow with two deployments. The second changes the agent prompt and adds a Slack step. No model or Slack call was executed for this capture. + +The media comes from the running product, captured through the browser. The video is a silent, edited walkthrough assembled from three unmodified UI captures with reading pauses; it is not a continuous screen recording. No generated controls or results are used. + +1. Open deployment history. +2. Open the version menu and choose Compare. +3. Inspect the changed prompt, added Slack step, and connection. + +Assets: `/changelog/compare-workflow-deployments-v1.mp4` and `/changelog/compare-workflow-deployments-v1.jpg`. The poster is the final comparison view. The short MP4 is served locally with the site; larger future recordings should use the approved media CDN. Keep filenames immutable. + +## Review notes + +The screenshot replaces the older deployment-history reference. The comparison is read-only and shows actual before/after fields. Copy covers both two-version comparison and draft-versus-live review; the media demonstrates the two-version path. The draft remains unchanged after opening and closing Compare. + +The entry keeps the historical release timestamp and RSS identity. Playback, mobile layout, asset responses, and content validation belong in the publication PR's validation record. Assign the release editor and feature reviewer in that PR; verify the public URL before distributing. + +## Announcement drafts + +Social: + +> See what changed before you deploy in Sim. Compare workflow versions, review prompt edits and new steps, and inspect changed connections. Demo and guide: https://www.sim.ai/changelog/compare-workflow-deployments + +Email subject: See what changed before you deploy + +Email body: + +> Open deployment history and choose Compare to review two workflow versions. You can also review a draft against the live workflow before redeploying. See the product walkthrough and setup guide: https://www.sim.ai/changelog/compare-workflow-deployments + +These are drafts, not sent announcements. diff --git a/apps/sim/content/changelog/editorial-standard.md b/apps/sim/content/changelog/editorial-standard.md new file mode 100644 index 00000000000..b2217463569 --- /dev/null +++ b/apps/sim/content/changelog/editorial-standard.md @@ -0,0 +1,49 @@ +# Changelog editorial and media standard + +Use this contract for the Sim drafting workflow and the final editorial review. The publishing guide owns the MDX format, media hosting, and release checks. This standard owns selection, copy, and demonstration quality. + +## Choose the right-sized story + +A standalone update needs a meaningful capability, a substantial improvement to an existing task, or an action-required notice. State what a team can do, the situation it helps with, and how the proposed media will demonstrate it. + +Routine retry fixes, loading improvements, internal refactors, and small polish belong in a related edition's short Improvements or Fixes section. Keep unrelated maintenance in the technical release history. A batch of commits is not a story. Defer a batch with only supporting notes instead of padding it into a launch. An urgent breaking change needs a timely notice and a verified explanatory image when there is no useful product screen. + +Keep existing article URLs, original dates, and RSS identities when refining historical copy. Do not merge existing entries just to make a larger announcement. Preserve material version, permission, and rollout restrictions. + +## Write a compact entry + +- Headline: name the capability or action, at most 65 characters. Avoid generic headlines such as “A better experience.” +- Summary: 15–40 words for search, sharing, and RSS, naming Sim and explaining the capability in plain language. It must make sense without the image. +- Body: normally 60–180 words, with one concrete example or short path to the feature. Explain the action and result; do not repeat the summary or turn a small improvement into a tutorial. +- Media: place the reviewed image or video immediately after the article header, before procedural detail. Images use the shared lightbox and can show a brief caption for a necessary demonstration limit. Videos keep native playback controls and an accessible description, without a repeated description beneath the player. Any material claim limitation belongs in the article's prose. +- Supporting notes: up to three related, source-backed improvements, each one sentence. Omit empty sections. Do not invent extras to fill a template. +- Links: one descriptive setup/documentation link, plus an integration, model, or product link when directly relevant. Use the actual catalog slug and guide. Put links in context; avoid a wall of generic “Learn more” links. +- Availability: retain real version, plan, permission, and rollout restrictions in the article. Keep unverified rollout questions, source analysis, capture instructions, and approval checkboxes in production.md. + +Use short active sentences and familiar words. Technical identifiers, retry timings, queue keys, PR numbers, and performance claims without measurements do not belong in the article. There is no minimum number of announcements per week. + +## Make the media prove one thing + +Choose the format before capture. A screenshot suits a state or configuration. A 20–45 second recording suits an interaction. A clearly labeled, verified diagram suits an invisible system change. Generated art can explain a concept, but cannot stand in for product evidence. + +Every production brief records: + +1. The exact claim and what the demonstration does not establish. +2. The starting state, permissions, sample data, action, and visible result. +3. The chosen format and, for video, intended duration and a short sequence of shots. +4. The result frame to use as a poster and the intended crop or framing. +5. The real capture environment, build, date, source files, and reviewed final filenames. + +Use the same clean demo workspace and product theme. Frame the relevant panel with enough context to locate it, and make the result legible at a 390px viewport. Hide unrelated private sidebar content before recording. Keep UI text and controls intact; never generate a different product state to improve a screenshot. Prefer recording at the intended framing over aggressive crops later. + +Begin with the task ready to perform, show the action, and hold on its result. Avoid decorative intros, music, title cards, and excessive cursor movement. Keep the headline and important explanation in HTML. Label videos assembled from still screenshots as edited walkthroughs. + +Choose the poster deliberately after reviewing the video. An opening menu, empty state, spinner, or arbitrary first second rarely explains the feature. The media workflow can provide candidate frames, but an editor selects the final timestamp. Preview candidates are not approved assets. Preserve the real aspect ratio inside the shared media frame; do not stretch product UI. Use the same reviewed result frame for the article poster and share image when it works in both contexts. + +The page's shared landing components supply typography, theme colors, spacing, and radius. Do not add a separate visual theme to each entry or bake decorative headings and chrome into captures. Provide descriptive alt text and reviewed WebVTT captions when there is speech. + +## Review the finished entry + +The workflow checks the proposed copy against source evidence in a second editorial pass, then validates structure, source references, the link allowlist, word budgets, and the media plan. These checks do not establish the truth of every claim or the quality of a capture. The editor and feature owner review the article and media together: accurate behavior, confirmed availability, useful next step, readable framing, correct poster, working playback and links, and consistent copy across the page and announcement drafts. + +Add approved media and index.mdx to the same draft PR. Keep draft: true until review is complete. Do not create placeholder media paths to make an incomplete brief look finished. Follow the publishing guide's content audit and normal repository checks, then verify the deployed article, media, RSS, and sitemap before distribution. diff --git a/apps/sim/content/changelog/maintenance-workflow.md b/apps/sim/content/changelog/maintenance-workflow.md new file mode 100644 index 00000000000..d49951507f9 --- /dev/null +++ b/apps/sim/content/changelog/maintenance-workflow.md @@ -0,0 +1,133 @@ +# Maintaining the changelog with Sim + +Status: operating design for the editorial workflows. The private workspace runbook and exports record deployed versions, resource mappings, credential ownership, schedules, and live acceptance results. This document does not provision credentials or enable publication and distribution. + +Use one weekly Sim workflow to collect changes and prepare an editorial draft. Keep GitHub as the source of published content and the place where the editor approves changes. Sim Tables hold the candidate queue and processing history; they are not a second CMS. The [publishing guide](./README.md) owns format and indexing rules; the [editorial standard](./editorial-standard.md) owns story selection, copy, and demonstration quality. + +## The first version + +Start with a weekly review, for example Monday at 09:00 in `America/Los_Angeles`. Run a manual catch-up for a significant launch or an urgent notice. A weekly collection is sufficient initially; add daily ingestion or GitHub webhooks only if the queue's volume or timeliness calls for them. Publishing is based on meaningful, available changes, not the calendar. + +```mermaid +flowchart LR + A[Weekly schedule] --> B[Collect GitHub evidence] + B --> C[Sim Tables candidate queue] + C --> D[Select and draft a story] + D --> R[Check copy against source evidence] + R --> E[One draft GitHub PR] + E --> F[Verify availability and real media] + F --> G[Review, merge, and deploy] +``` + +| Stage | Sim capability | Configuration and result | +| --- | --- | --- | +| Start | Schedule | Weekly, with an explicit timezone. The schedule starts when the workflow is deployed. | +| Collect | API, Loop, Function | Read paginated GitHub PRs and selected source files. Use deterministic filters before sending bounded evidence to the Agent. | +| Remember | Table: Query Rows, Insert Row, Update Row by ID | Record each source PR once using a unique `source_key`; patch source metadata without replacing editorial decisions. | +| Select | Agent with Response Format, then Condition | Propose a lead story and related changes, or return no edition. Record the reason and missing evidence. | +| Prepare | Drafting Agent, editing Agent, and Function | Draft the benefit, availability questions, contextual links, demo shot list, captions/alt text, and announcement copy. A second Agent checks the copy against source evidence. Validate fields and paths outside the models. | +| Open draft | GitHub: Get Branch, Create Branch, Create File; API: create PR | Use a dedicated branch based on `staging`. Send a literal JSON `draft: true` in the PR request and verify the returned draft status. Never write directly to `staging` or merge automatically. | +| Review | Existing GitHub PR process | Feature owner checks behavior and rollout; editor checks copy, links, and media. Keep a single approval surface initially. | +| Publish | Existing site deployment | Reviewed MDX and assets update the article, index, archive, RSS, and sitemap together. Confirm the live URL before marking the edition published. | + +Sim also has Human in the Loop if the team later wants an approval portal. It is optional here: adding a second approval queue alongside GitHub would add work without improving the first version. + +## State and catch-up + +Create three small tables in the selected Sim workspace: + +- `changelog_candidates`: unique `source_key` such as `simstudioai/sim#123`; source URL, merge SHA/time, source update time, proposed benefit, decision, decision reason, owner, availability evidence, related URLs, and edition key. Decisions are `new`, `deferred`, `selected`, `skipped`, or `published`. Keep an editor's decision when source metadata is refreshed. +- `changelog_editions`: unique `edition_key`; selected source keys, branch, draft PR URL, source snapshot, media status, review status, canonical URL, and publication status. Choose the key once and reuse it on retries; do not derive a new key from each run's time or generated headline. +- `changelog_runs`: unique run key; scan lower/upper bounds, last fully collected boundary, run result, counts, and error details. Collection progress is separate from publication status. + +Use unique columns for deduplication. Query by `source_key`, then use Insert Row for a new candidate or Update Row by ID with only collector-owned fields for an existing one. If a concurrent insert wins the unique key, reread that candidate and patch its source metadata. Never reset review decisions or publication status. Upsert Row replaces the entire stored row when a key matches, so it is unsuitable for partial refreshes; even merging a previously read snapshot could overwrite a newer editor decision. + +Apply the same insert/partial-update rule to edition and run records. Query table pages with a limit and follow `nextCursor` until it is null. A short Table page can reflect the response byte budget rather than the end of the results. + +The first run needs a deliberate start date. Seed already-covered source PRs from the existing production briefs, or have the editor review the initial backfill before any draft PR is created. Subsequent scans start at the last fully collected boundary with an overlap, such as 48 hours. Deduplicate the overlap by source key. Revisit deferred candidates separately so an older feature can become a story when its rollout is ready. + +For GitHub collection, use the API block against the PR list endpoint with `state=closed`, `base=staging`, `sort=updated`, `direction=desc`, and `per_page=100`. Follow pagination until the stored scan boundary is covered; filter out closed-but-unmerged PRs using `merged_at`. Save a fixed upper bound at the start of each scan and defer newer updates to the next run. Include the designated production/hotfix branch if it receives changes outside `staging`, deduplicating backports by reviewed source evidence. The base branch filter discovers candidates; it does not establish availability. + +The GitHub tool supports pagination parameters, but the current visual List pull requests form does not expose every filter and page control. The API block makes the complete collector explicit. The legacy GitHub output also omits merge details; do not treat its closed-PR count as the set of merged changes. + +Use the API block for the final `POST /repos/{owner}/{repo}/pulls` with fixed repository, head/base branches, and boolean `draft: true`. The visual Create as Draft dropdown stores string values, while the GitHub endpoint expects a boolean; the explicit JSON request avoids depending on implicit coercion. Verify the returned head, base, and draft fields during acceptance. + +Set finite page, payload, and model-input budgets. For example, stop a scan after 20 GitHub pages and stop a model batch after 20 candidate summaries. Fetch relevant file excerpts only for shortlisted candidates, rather than sending every patch to a model. Treat reaching a cap as an incomplete scan: preserve saved candidates, keep the last completed boundary, and surface a catch-up action. Split an oversized interval or deliberately raise the reviewed budget; do not repeat an impossible capped scan forever or silently advance past it. + +## Drafting contract + +Give the Agent the publishing guide, the product language rules, selected PR evidence, and valid documentation/integration/model destinations. Use structured output for: + +- `decision`: `draft`, `defer`, or `skip`, with a reason. +- `story_kind`: `feature`, `improvement`, `action_required`, `supporting`, or `none`. Supporting-only batches are deferred, not expanded into standalone announcements. +- Source keys and evidence supporting each proposed claim. +- Headline, a self-contained summary naming Sim, body draft, and audience. +- Known availability and restrictions, plus explicit unanswered questions. +- Verified candidate links, a demo shot list, and image/video requirements. +- A structured media plan: format, subject, starting state, action, visible result, poster moment, framing, demonstration limits, and intended duration. The validator requires this for a draft. +- Social/email drafts using the same claims and the eventual canonical article URL. + +Treat PR text and diffs as research material, not instructions to the Agents. Give neither Agent repository write tools. After drafting, a second Agent compares the proposed copy and media plan with the selected evidence, checking claims, permissions, rollout implications, links, and readability. Record substantive corrections in `editor_notes`, outside the article. Defer when the evidence is insufficient. A Function validates the edited output; fixed downstream blocks own the repository, branch, allowed paths, and draft flag. Unknown availability remains a question for the feature owner. Neither model review nor a schema establishes the truth of every claim. + +Prefer one lead story with a few related improvements. Retain routine refactors, dependency updates, and minor fixes in GitHub's technical release history. Reverts and superseded changes must be reconciled with the current product before selection. Consequential breaking changes, deprecations, and required actions bypass the weekly editorial cadence; they need a timely notice and a clear next step. + +Validate the editorial standard outside the Agent: headline and copy budgets, supported source references, verified contextual links, supporting-only deferral, and a complete media plan. Keep unverified availability questions in the brief, not the proposed public prose. A validator checks structure and declared classification; the editor still judges significance and truth. Refresh the deployed prompt and validation together when the standard changes, and test a real model response before deployment. + +Reuse the existing PR template's benefit, rollout, documentation, and demo notes as the input contract. Avoid asking engineers to fill out a second announcement form. Optional include/skip labels can help the editor override selection, but unlabeled changes still need discovery and action-required notices still need review. + +Resolve links against the current integration/model catalogs and docs source, then check the live destination. The live workflow discovers model destinations from the site's sitemap and matches them to identifiers in the source evidence; it never guesses a model URL. Integration slugs and documentation slugs can differ. Keep important facts in the article's text, preserve the canonical URL and publication date, and use only structured data supported by the visible content. + +The first draft PR can contain only `production.md`: proposed article text, source references, unresolved questions, shot list, and announcement drafts. Add `index.mdx` with `draft: true` once a real cover exists. The content audit requires a real local cover even for drafts; an invented filename or unrelated placeholder would create a failing PR. Keep secrets, unpublished strategy, and private operational evidence out of both files: non-rendered briefs are still public in this repository. + +## Media without a separate production project + +The feature owner supplies one real screenshot or a focused 20–45 second recording from a dedicated demo workspace. Reuse the product's current theme and typography. The workflow prepares the steps and copy; the owner confirms the resulting screen actually demonstrates the claim. + +From that approved capture, prepare the optimized MP4 when applicable, a local poster/cover, descriptive alt text, and WebVTT captions if there is speech. Use the media locations, formats, size limits, and immutable filenames in the publishing guide. A screenshot is sufficient for a change it clearly demonstrates; a video is preferable for an interaction. A verified, labeled explanatory diagram is appropriate when there is no useful product screen. + +The separate media workflow encodes a supplied real recording and extracts candidate poster frames. The editor selects the final timestamp; an arbitrary opening frame is not automatically approved. Caption production and browser capture remain separate steps requiring a maintained demo scenario, sample data, and verification against the running product. AI-generated product controls or results are never evidence. Review captions and the final compressed asset before use. + +Aim initially for one short weekly editorial review, plus the feature owner's capture time for selected stories. Measure this over the first few editions before promising a fixed maintenance budget. Automate repeated capture scenarios after they prove stable. + +## Retries and editor ownership + +- Before creating an edition, inspect its table record, branch, and open PR. Reuse an existing draft; avoid weekly duplicate PRs while it awaits review. New candidates stay in the queue. +- Create one deterministic branch per edition, such as `codex/changelog-2026-10-12`. If branch creation reports it already exists, reconcile the existing work before writing. Concurrent runs must stop or join that edition, not create another branch with a random suffix. +- Create files serially and open the PR only after all intended files exist. If a write's outcome is unknown, read the branch/file/PR before retrying. Resume missing steps; do not overwrite existing draft text. A partial failure must remain recoverable in the edition record. +- Once a person edits the draft, the workflow leaves those files alone. Regeneration requires an explicit editor request and a comparison with the recorded generated revision. Never force-push over review work. +- Retry transient reads with bounded backoff. Honor rate-limit responses. Leave non-idempotent POST/PATCH retries off unless the operation has been reconciled by its stable identity. A failed collection is a failed run, not an empty successful edition. +- A merged PR is not yet a published edition. Confirm the production article and media, RSS, and sitemap after site deployment, then mark the included candidates published. If deployment fails, retain a pending-deployment state. +- Correct published claims in place with a substantive `updated` date. Preserve original slugs, publication dates, and RSS identities. Do not regenerate historical entries every week. + +## Keep the workflow itself maintained + +Name one release editor, a backup, and an owner for the workflow's credentials. Configure credentials through Sim's supported secret/credential inputs. Restrict the GitHub credential to the intended repository and the permissions needed to read sources, write a draft branch, and open PRs. Keep branch protection and required review in place; the drafting workflow never receives a merge step. + +Use Sim Workspace Events to watch the drafter for Run Error. For missed weekly runs, use a separate daily Schedule → Table query → Condition check against the last successful collection, with an eight-day threshold. The current No Activity subscription is capped at 168 hours, so an eight-day setting would be clamped and could create false alarms around a weekly schedule or daylight-saving change. No Activity can instead watch this daily health check with a 30-hour threshold. A successful scan with no worthwhile story is healthy; a missing run is not. + +Sim's schedule disables after 100 consecutive failures, so do not rely on that limit as your first alert. Choose the owner and notification destination during setup. Keep routine runs quiet and deduplicate failure reminders; notify on a new reviewable draft, an actionable failure, or an overdue review. Keep a monthly human check of the workflow's deployment and credentials as a fallback if the workspace's scheduler or alert delivery fails. + +The release editor checks the queue weekly and resolves old deferred items. Monthly, check public links, media playback/range requests, caption delivery, and representative pages in Search Console and Bing Webmaster Tools. Source-level content validation cannot prove production playback or search-engine indexing. The workflow export and its configuration notes should be versioned without credentials so another maintainer can restore it. + +## Setup and acceptance + +1. Select the Sim workspace, editor/backup, GitHub credential, notification destination, timezone, and first collection boundary. +2. Create the three tables and unique keys. Build the Schedule → collection → queue → drafting path without repository writes, and run it manually on a small known interval. +3. Compare collected candidates with GitHub, including more than one page. Confirm already-covered PRs and closed-but-unmerged PRs do not create new stories. Check a deferred feature, a revert, and a week with no meaningful update. +4. Add draft-branch/file/PR steps. Exercise an overlapping candidate refresh after an editor changes its decision, two inserts racing for the same source key, a retry after a partial file write, an existing PR, concurrent start, and an editor-modified file. Verify saved decisions survive and each edition produces one recoverable draft without losing edits. +5. Finish one edition with real media. Run the content audit and existing PR gates, review locally through the development-only preview, then verify the deployed public URL, media, RSS, and sitemap. Hosted production builds intentionally do not expose draft preview routes. +6. Exercise a read failure, rate limit, page cap, expired credential, failed deployment, and missed schedule. Record the results and run links, verify the chosen alert path, then deploy the weekly workflow. + +Start with this draft-only version for the first two editions. Automate media processing or distribution only after the actual recurring work is clear. The publishing guide and PR template already supply the editorial contract; the new work is wiring the collector, state, draft preparation, and monitoring in the selected workspace. + +For an editor without a local development setup, have the feature owner attach a rendered article preview to the PR initially. An authenticated hosted preview would remove that handoff and should come before automated distribution. It is an additional feature to implement; keep draft pages out of public discovery, sitemap, and RSS when adding it. + +## Capability references + +- [Schedule and deployment](https://docs.sim.ai/workflows/triggers/schedule) +- [GitHub integration](https://docs.sim.ai/integrations/github) and [GitHub PR list pagination](https://docs.github.com/en/rest/pulls/pulls#list-pull-requests) +- [API requests and retries](https://docs.sim.ai/workflows/blocks/api) +- [Tables in workflows](https://docs.sim.ai/tables/using-in-workflows) +- [Agent structured output](https://docs.sim.ai/workflows/blocks/agent) +- [Human in the Loop](https://docs.sim.ai/workflows/blocks/human-in-the-loop) +- [Sim Workspace Events](https://docs.sim.ai/workflows/triggers/sim) diff --git a/apps/sim/content/changelog/more-reliable-runs/index.mdx b/apps/sim/content/changelog/more-reliable-runs/index.mdx new file mode 100644 index 00000000000..89581b87bd8 --- /dev/null +++ b/apps/sim/content/changelog/more-reliable-runs/index.mdx @@ -0,0 +1,26 @@ +--- +slug: more-reliable-runs +title: 'More reliable runs and file handling' +description: 'Sim improves how Function blocks handle workflow references, retains file origins through exports, and keeps background confirmations attached to their runs.' +date: '2026-10-05T08:11:57Z' +authors: [waleed] +tags: [Files, Workflows, Improvements] +ogImage: /blog/secret-provenance/cover.jpg +ogAlt: 'Tracking file and secret origins through an agent run in Sim' +technical: false +draft: true +release: + versions: [v0.9.14] + url: https://github.com/simstudioai/sim/releases/tag/v0.9.14 +--- + +### Improvements + +- Large [Function blocks](https://docs.sim.ai/workflows/blocks/function) handle workflow references more efficiently. +- [Secret tracking](/blog/secret-provenance) retains known file origins through binary exports and archive extraction. + +### Fixes + +- Background workflow confirmations now preserve the identity of the workflow run they belong to. + +Explore [Files in Sim](/files) and the guide to [using files in workflows](https://docs.sim.ai/files/using-in-workflows). diff --git a/apps/sim/content/changelog/more-reliable-runs/production.md b/apps/sim/content/changelog/more-reliable-runs/production.md new file mode 100644 index 00000000000..98bba106951 --- /dev/null +++ b/apps/sim/content/changelog/more-reliable-runs/production.md @@ -0,0 +1,24 @@ +# Reliability improvements: editorial candidate + +Status: draft; select for a themed edition rather than publishing solely because v0.9.14 shipped. +Feature owner: assign in the publication PR. Editor: assign in the publication PR. + +## Story selection + +The source is [v0.9.14](https://github.com/simstudioai/sim/releases/tag/v0.9.14). The current draft combines code handling, file provenance, and background confirmations. Confirm a clear audience and a practical example before treating that collection as a standalone story. Otherwise, move the relevant notes into a related feature edition and keep the remaining changes in the technical history. + +## Media assessment + +`/blog/secret-provenance/cover.jpg` is the existing secret-provenance article's illustration. It is not a product screenshot and does not demonstrate the new export or archive behavior. It may illustrate the broader subject, but it is insufficient as the sole evidence for this release. Keep the entry in draft. + +If file provenance becomes the lead story, demonstrate an export or archive extraction with safe sample files and the actual resulting origin information in Sim. Capture only information the product exposes. Alternatively, use an explicitly labeled explanatory diagram whose transformations have been verified against the feature implementation. Link to the [secret provenance article](https://www.sim.ai/blog/secret-provenance) and [Files guide](https://docs.sim.ai/files/using-in-workflows). + +Do not invent a speedup percentage or synthetic before-and-after timing for the code-handling improvement. Any performance claim needs a repeatable measurement and stated workload. + +## Completion criteria + +- [ ] Editor selects one meaningful lead story or folds these notes into a related edition. +- [ ] Feature owner verifies the demonstrated behavior and deployed availability. +- [ ] The article has a relevant, reviewed image or video; cover and caption describe it accurately. +- [ ] Capture environment, date, steps, source asset, and final filenames are recorded here. +- [ ] Links, mobile readability, content audit, and distribution copy are reviewed. diff --git a/apps/sim/content/changelog/power-bi-for-agents/index.mdx b/apps/sim/content/changelog/power-bi-for-agents/index.mdx new file mode 100644 index 00000000000..bf9ec1e78e8 --- /dev/null +++ b/apps/sim/content/changelog/power-bi-for-agents/index.mdx @@ -0,0 +1,28 @@ +--- +slug: power-bi-for-agents +title: 'Bring Power BI data into your agents' +description: 'Query Power BI data from Sim and pass the results to an agent for a briefing or follow-up. Browse reports and manage refreshes in the same workflow.' +date: '2026-10-03T04:32:55Z' +authors: [waleed] +tags: [Integrations] +ogImage: /changelog/power-bi-query-v1.jpg +ogAlt: 'Power BI block configured with a sample sales query in the Sim workflow builder' +technical: false +draft: false +release: + versions: [v0.9.12] +--- + + + +Add [Power BI](/integrations/power-bi) to a workflow, connect your organizational Microsoft account, and choose a semantic model. Use DAX, Power BI's query language, to fetch the rows an agent needs. For example, query revenue by region to prepare a weekly sales briefing. + +You can also browse reports, request a model refresh, and check its progress through refresh history. + +Included in [v0.9.12](https://github.com/simstudioai/sim/releases/tag/v0.9.12). Queries require model Read and Build permissions and your tenant's Execute Queries setting. The [Power BI setup guide](https://docs.sim.ai/integrations/powerbi) covers permissions and supported actions. diff --git a/apps/sim/content/changelog/power-bi-for-agents/production.md b/apps/sim/content/changelog/power-bi-for-agents/production.md new file mode 100644 index 00000000000..cb37224b2a5 --- /dev/null +++ b/apps/sim/content/changelog/power-bi-for-agents/production.md @@ -0,0 +1,35 @@ +# Power BI: production record + +Status: prepared for publication review. Only `index.mdx` is rendered publicly. + +## Evidence and availability + +[PR #8538](https://github.com/simstudioai/sim/pull/8538), included in [v0.9.12](https://github.com/simstudioai/sim/releases/tag/v0.9.12). Reviewed the block, action implementations, and [setup guide](https://docs.sim.ai/integrations/powerbi). + +Eight actions cover workspaces, report metadata, semantic-model metadata, DAX queries, refresh requests, and refresh history. An accepted refresh request is not a completed refresh. The public copy preserves this distinction and the query permission requirements. + +The entry links to `/integrations/power-bi` and the matching docs guide at `/integrations/powerbi`. It has its own RSS identity because the fork-comparison story already carries the historical v0.9.12 release identity. + +## Media provenance + +Captured October 7, 2026, from the actual local self-hosted app based on staging commit `684b228623`, using a dedicated sample workspace. Asset: `/changelog/power-bi-query-v1.jpg`, 1440×900. + +The screenshot shows the Power BI block and a synthetic DAX query using sample sales table and column names. The account, workspace, and model selectors are intentionally unconnected. No Microsoft account was connected and no query or refresh was executed. The article explicitly describes a configuration example, not a successful provider call. Do not add a results screen without a real authorized connection and a verified run. + +The sample demonstrates where the query is configured. Actual table and column names must match the reader's model. The setup guide describes organizational accounts, delegated connection requirements, permission limits, and unsupported actions. + +## Review and distribution + +Assign the release editor and feature reviewer in the publication PR. Verify the local asset, mobile layout, canonical links, and public URL before distribution. Local availability does not independently verify every Cloud rollout. + +Social draft: + +> Bring Power BI data into your agents in Sim. Query semantic models, inspect reports, and request refreshes from a workflow block. Setup requirements and example: https://www.sim.ai/changelog/power-bi-for-agents + +Email subject: Connect your agents to Power BI + +Email body: + +> Sim's Power BI integration can query semantic models, inspect reports, and request refreshes. Use the returned rows in a briefing or follow-up. See the configuration example and Microsoft connection requirements: https://www.sim.ai/changelog/power-bi-for-agents + +These are drafts, not sent announcements. diff --git a/apps/sim/content/changelog/review-fork-changes/index.mdx b/apps/sim/content/changelog/review-fork-changes/index.mdx new file mode 100644 index 00000000000..8344f163e92 --- /dev/null +++ b/apps/sim/content/changelog/review-fork-changes/index.mdx @@ -0,0 +1,27 @@ +--- +slug: review-fork-changes +title: 'Review fork changes before syncing' +description: 'See what changed in a source workflow before syncing a workspace fork in Sim. Review prompts, settings, and steps against the last successful sync.' +date: '2026-10-03T04:32:55Z' +authors: [waleed] +tags: [Workspaces, Enterprise] +ogImage: /changelog/review-fork-changes-v1.jpg +ogAlt: 'Last Sync to Now comparison showing a changed renewal-reminder setting in Sim' +technical: false +draft: false +release: + versions: [v0.9.12] + url: https://github.com/simstudioai/sim/releases/tag/v0.9.12 +--- + + + +Open **Workspace forks → Edit mappings**, select **Push** or **Pull**, then choose **Compare** from a deployed workflow's menu. **Last Sync → Now** shows the source changes since that fork's last successful sync. + +Each fork and direction keeps its own comparison history. If no earlier snapshot is available, Sim explains why it cannot show a comparison. + +Included in **v0.9.12**. Requires workspace admin access and Forks enabled: Sim Cloud Enterprise with organization access, or a self-hosted deployment. Follow the [workspace forks guide](https://docs.sim.ai/platform/enterprise/forks) to set up mappings and sync workflows. diff --git a/apps/sim/content/changelog/review-fork-changes/production.md b/apps/sim/content/changelog/review-fork-changes/production.md new file mode 100644 index 00000000000..72eafce3bd8 --- /dev/null +++ b/apps/sim/content/changelog/review-fork-changes/production.md @@ -0,0 +1,41 @@ +# Fork comparison: production record + +Status: prepared for publication review. Only `index.mdx` is rendered publicly. + +## Evidence and availability + +[PR #8586](https://github.com/simstudioai/sim/pull/8586), commit `405cc6a835`, included in [v0.9.12](https://github.com/simstudioai/sim/releases/tag/v0.9.12). Checked the source and the actual fork comparison in the local self-hosted app based on staging commit `684b228623` on October 7, 2026. + +A dedicated sample workspace was forked through the real application operation. A real sync and its deployment outbox completed before creating the next source deployment. The subsequent preview reported an available baseline between source versions 1 and 2. This avoids inventing a Last Sync snapshot or displaying a first-sync screen as the comparison feature. + +The public copy preserves workspace admin access, Cloud Enterprise enablement, and self-hosted Forks enablement requirements from the [workspace forks guide](https://docs.sim.ai/platform/enterprise/forks). Local verification does not establish each Cloud organization's rollout. + +## Media provenance + +The running product shows a sample renewal-reminder workflow changing from seven to fourteen days and adding an account-summary setting. No customer data or external message is used. + +1. Open the fork's parent mapping settings and select Pull. +2. Choose Compare from the deployed workflow's menu. +3. Review Last Sync → Now and the actual changed code. + +Assets: `/changelog/review-fork-changes-v1.jpg` and `/changelog/review-fork-changes-v1.mp4`. The poster shows the final comparison. The silent video is an edited walkthrough assembled from three unmodified UI captures with reading pauses, not a continuous screen recording. It uses the actual product UI; no controls or results are generated. + +The previous mapping-only reference image has been removed. The new screenshot proves the announced comparison. The capture stops before performing another sync. + +## Review and edge cases + +The baseline records a successful destination activation, is specific to the source/destination relationship, and can be unavailable before the first sync or when an older snapshot has been removed. The article explains those limits. Comparison does not claim to preview destination edits or resource mappings. + +Assign the release editor and feature reviewer in the publication PR. Validate playback, asset responses, responsive layout, links, and the public URL before distribution. Keep the historical timestamp and v0.9.12 RSS identity stable. + +Social draft: + +> Review changes before syncing a workspace fork in Sim. Last Sync → Now shows what changed in the source workflow. Available with Workspace Forks; setup and demo: https://www.sim.ai/changelog/review-fork-changes + +Email subject: Review fork changes before syncing + +Email body: + +> Open your fork's mapping settings and choose Compare from a deployed workflow's menu. Inspect source changes since the last successful sync before updating the fork. Availability and demo: https://www.sim.ai/changelog/review-fork-changes + +These are drafts, not sent announcements. diff --git a/apps/sim/lib/changelog/constants.ts b/apps/sim/lib/changelog/constants.ts new file mode 100644 index 00000000000..e56303f5ff5 --- /dev/null +++ b/apps/sim/lib/changelog/constants.ts @@ -0,0 +1,11 @@ +import type { ContentSection } from '@/lib/content/seo' + +export const CHANGELOG_SECTION = { + name: 'Changelog', + basePath: '/changelog', + description: + 'New ways to build, deploy, and manage AI agents in Sim, plus improvements and fixes.', + speakableSelectors: ['[itemprop="headline"]'], +} satisfies ContentSection + +export const LATEST_ENTRY_LIMIT = 12 diff --git a/apps/sim/lib/changelog/index.ts b/apps/sim/lib/changelog/index.ts new file mode 100644 index 00000000000..c0c9943b5d8 --- /dev/null +++ b/apps/sim/lib/changelog/index.ts @@ -0,0 +1,2 @@ +export { CHANGELOG_SECTION, LATEST_ENTRY_LIMIT } from './constants' +export { getAllEntryMeta, getAllEntryPreviews, getEntryBySlug, getEntryPreview } from './registry' diff --git a/apps/sim/lib/changelog/mdx.tsx b/apps/sim/lib/changelog/mdx.tsx new file mode 100644 index 00000000000..770642d458d --- /dev/null +++ b/apps/sim/lib/changelog/mdx.tsx @@ -0,0 +1,47 @@ +import type { ComponentPropsWithoutRef } from 'react' +import { cn } from '@sim/emcn' +import type { MDXRemoteProps } from 'next-mdx-remote/rsc' +import { ChangelogImage, ChangelogVideo } from '@/app/(landing)/changelog/components' +import { HOME_TYPE } from '@/app/(landing)/components/landing-layout' +import { PROSE_TYPE } from '@/app/(landing)/components/prose-page/constants' + +export const changelogComponents = { + ChangelogVideo, + ChangelogImage, + h3: ({ className, ...props }: ComponentPropsWithoutRef<'h3'>) => ( +

+ ), + h4: ({ className, ...props }: ComponentPropsWithoutRef<'h4'>) => ( +

+ ), + p: ({ className, ...props }: ComponentPropsWithoutRef<'p'>) => ( +

+ ), + ul: ({ className, ...props }: ComponentPropsWithoutRef<'ul'>) => ( +

    + ), + ol: ({ className, ...props }: ComponentPropsWithoutRef<'ol'>) => ( +
      + ), +} satisfies MDXRemoteProps['components'] diff --git a/apps/sim/lib/changelog/media.test.ts b/apps/sim/lib/changelog/media.test.ts new file mode 100644 index 00000000000..0f0733e2af6 --- /dev/null +++ b/apps/sim/lib/changelog/media.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest' +import { isChangelogMediaSource } from '@/lib/changelog/media' + +describe('changelog media origins', () => { + it.each([ + { kind: 'tab padding', source: '/\t/example.test/demo.mp4' }, + { kind: 'newline padding', source: '/\n//example.test/demo.mp4' }, + { kind: 'carriage-return padding', source: '/\r/example.test/demo.mp4' }, + { kind: 'backslash separators', source: '/\\example.test/demo.mp4' }, + ])('rejects $kind that browsers resolve to another origin', ({ source }) => { + expect(new URL(source, 'https://www.sim.ai').origin).toBe('https://example.test') + expect(isChangelogMediaSource(source)).toBe(false) + }) +}) diff --git a/apps/sim/lib/changelog/media.ts b/apps/sim/lib/changelog/media.ts new file mode 100644 index 00000000000..4c443037411 --- /dev/null +++ b/apps/sim/lib/changelog/media.ts @@ -0,0 +1,14 @@ +/** Public asset origin already used by Sim's Academy recordings. */ +export const CHANGELOG_MEDIA_ORIGIN = 'https://nnjgp7vypgx4myuq.public.blob.vercel-storage.com' + +/** Shared by rendering and content validation; CSP imports this leaf before aliases resolve. */ +export function isChangelogMediaSource(source: string): boolean { + if (typeof source !== 'string' || /[\r\n\t\\]/.test(source)) return false + if (source.startsWith('/') && !source.startsWith('//')) return true + try { + const url = new URL(source) + return url.origin === CHANGELOG_MEDIA_ORIGIN && !url.username && !url.password + } catch { + return false + } +} diff --git a/apps/sim/lib/changelog/registry.ts b/apps/sim/lib/changelog/registry.ts new file mode 100644 index 00000000000..9b53dd20aa1 --- /dev/null +++ b/apps/sim/lib/changelog/registry.ts @@ -0,0 +1,38 @@ +import path from 'node:path' +import { CHANGELOG_SECTION } from '@/lib/changelog/constants' +import { changelogComponents } from '@/lib/changelog/mdx' +import { createContentRegistry } from '@/lib/content/registry-factory' + +const registry = createContentRegistry({ + contentDir: path.join(process.cwd(), 'content', 'changelog'), + authorsDir: path.join(process.cwd(), 'content', 'authors'), + basePath: CHANGELOG_SECTION.basePath, + components: changelogComponents, + scopeHeadingIds: true, +}) + +/** Public consumers always receive published entries. */ +export async function getAllEntryMeta() { + return registry.getAllPostMeta() +} + +/** Returns only published updates, including for direct links. */ +export async function getEntryBySlug(slug: string) { + const entries = await getAllEntryMeta() + if (!entries.some((entry) => entry.slug === slug)) return null + return registry.getPostBySlug(slug) +} + +/** Draft previews are local-only and reread metadata after editorial changes. */ +export async function getAllEntryPreviews() { + if (process.env.NODE_ENV !== 'development') return [] + registry.invalidateCaches() + return registry.getAllPostMeta({ includeDrafts: true }) +} + +/** Never expose an unpublished body outside the development preview. */ +export async function getEntryPreview(slug: string) { + if (process.env.NODE_ENV !== 'development') return null + registry.invalidateCaches() + return registry.getPostBySlug(slug) +} diff --git a/apps/sim/lib/content/registry-factory.ts b/apps/sim/lib/content/registry-factory.ts index 120872ea73c..c1c1b0b9199 100644 --- a/apps/sim/lib/content/registry-factory.ts +++ b/apps/sim/lib/content/registry-factory.ts @@ -4,6 +4,7 @@ import { cache } from 'react' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import matter from 'gray-matter' +import type { MDXRemoteProps } from 'next-mdx-remote/rsc' import { compileMDX } from 'next-mdx-remote/rsc' import rehypeAutolinkHeadings from 'rehype-autolink-headings' import rehypeSlug from 'rehype-slug' @@ -31,10 +32,14 @@ export interface ContentRegistryConfig { basePath: string /** Per-slug custom MDX component overrides, merged over the base `mdxComponents` map. */ componentLoaders?: ContentComponentLoaders + /** Shared MDX components for this section, overridden by per-post components. */ + components?: MDXRemoteProps['components'] + /** Prefixes heading IDs with the post slug when a collection renders multiple bodies. */ + scopeHeadingIds?: boolean } export interface ContentRegistry { - getAllPostMeta: () => Promise + getAllPostMeta: (options?: { includeDrafts?: boolean }) => Promise getPostBySlug: (slug: string) => Promise /** Raw markdown body (frontmatter stripped) of a published post, or null if none. */ getPostSource: (slug: string) => Promise @@ -89,7 +94,14 @@ async function loadAuthorsForDir(authorsDir: string): Promise> = {} let metaPromise: Promise | null = null @@ -194,6 +206,7 @@ export function createContentRegistry(config: ContentRegistryConfig): ContentReg wordCount, draft: fm.draft, featured: fm.featured ?? false, + release: fm.release, technical: fm.technical, } }) @@ -201,8 +214,12 @@ export function createContentRegistry(config: ContentRegistryConfig): ContentReg return results.filter((result): result is ContentMeta => result !== null).sort(byDateDesc) } - async function getAllPostMeta(): Promise { - return (await scanFrontmatters()).filter((p) => !p.draft) + async function getAllPostMeta({ + includeDrafts = false, + }: { + includeDrafts?: boolean + } = {}): Promise { + return (await scanFrontmatters()).filter((p) => includeDrafts || !p.draft) } /** @@ -274,7 +291,8 @@ export function createContentRegistry(config: ContentRegistryConfig): ContentReg const fm = ContentFrontmatterSchema.parse(data) const postComponents = await loadPostComponents(slug) - const mergedComponents = { ...mdxComponents, ...postComponents } + const mergedComponents = { ...mdxComponents, ...components, ...postComponents } + const headingPrefix = scopeHeadingIds ? `${slug}-` : '' const compiled = await compileMDX({ source: content, @@ -284,7 +302,7 @@ export function createContentRegistry(config: ContentRegistryConfig): ContentReg mdxOptions: { remarkPlugins: [remarkGfm], rehypePlugins: [ - rehypeSlug, + [rehypeSlug, { prefix: headingPrefix }], [rehypeAutolinkHeadings, { behavior: 'wrap', properties: { className: 'anchor' } }], ], }, @@ -296,7 +314,7 @@ export function createContentRegistry(config: ContentRegistryConfig): ContentReg const match = /^##\s+(.+)$/.exec(line.trim()) if (match) { const text = match[1].trim() - headings.push({ text, id: slugifyHeading(text) }) + headings.push({ text, id: `${headingPrefix}${slugifyHeading(text)}` }) } } return { diff --git a/apps/sim/lib/content/schema.ts b/apps/sim/lib/content/schema.ts index c7737f6bcfb..8b187acb9f7 100644 --- a/apps/sim/lib/content/schema.ts +++ b/apps/sim/lib/content/schema.ts @@ -41,6 +41,13 @@ export const ContentFrontmatterSchema = z .optional(), draft: z.boolean().default(false), featured: z.boolean().default(false), + release: z + .object({ + versions: z.array(z.string().min(1)).min(1), + url: z.string().url().optional(), + }) + .strict() + .optional(), /** * Whether this post covers technical/developer content (architecture, * implementation, how-tos). Drives whether `TechArticle` is included @@ -76,6 +83,7 @@ export interface ContentMeta { canonical: string draft: boolean featured: boolean + release?: { versions: string[]; url?: string } technical: boolean } diff --git a/apps/sim/lib/content/seo.ts b/apps/sim/lib/content/seo.ts index 6d6337d58eb..385aaa3082d 100644 --- a/apps/sim/lib/content/seo.ts +++ b/apps/sim/lib/content/seo.ts @@ -17,6 +17,8 @@ export interface ContentSection { basePath: string /** Collection-page description used in `CollectionPage` JSON-LD. */ description: string + /** Visible article text available for spoken summaries; defaults to headline and description. */ + speakableSelectors?: readonly string[] } export function buildPostMetadata(post: ContentMeta): Metadata { @@ -84,7 +86,10 @@ export function buildPostMetadata(post: ContentMeta): Metadata { * posts that are genuinely technical/developer content (`post.technical`) — * general announcements (funding, company news) get `BlogPosting` alone. */ -export function buildArticleJsonLd(post: ContentMeta) { +export function buildArticleJsonLd( + post: ContentMeta, + speakableSelectors: readonly string[] = ['[itemprop="headline"]', '[itemprop="description"]'] +) { return { '@type': post.technical ? ['BlogPosting', 'TechArticle'] : 'BlogPosting', url: post.canonical, @@ -130,7 +135,7 @@ export function buildArticleJsonLd(post: ContentMeta) { inLanguage: 'en-US', speakable: { '@type': 'SpeakableSpecification', - cssSelector: ['[itemprop="headline"]', '[itemprop="description"]'], + cssSelector: speakableSelectors, }, } } @@ -165,7 +170,7 @@ export function buildFaqJsonLd(items: { q: string; a: string }[] | undefined) { export function buildPostGraphJsonLd(post: ContentMeta, section: ContentSection) { const graph: Record[] = [ - buildArticleJsonLd(post), + buildArticleJsonLd(post, section.speakableSelectors), buildBreadcrumbJsonLd(post, section), ] diff --git a/apps/sim/lib/core/security/csp.ts b/apps/sim/lib/core/security/csp.ts index f2d3c558573..d31e6e5bc2d 100644 --- a/apps/sim/lib/core/security/csp.ts +++ b/apps/sim/lib/core/security/csp.ts @@ -1,3 +1,4 @@ +import { CHANGELOG_MEDIA_ORIGIN } from '../../changelog/media' import { CONSENT_BACKEND_URL } from '../../consent/constants' import { env, envBoolean, getEnv } from '../config/env' import { isDev, isHosted, isReactGrabEnabled } from '../config/env-flags' @@ -124,6 +125,7 @@ const STATIC_IMG_SRC = ["'self'", 'data:', 'blob:', 'https:'] as const const STATIC_CONNECT_SRC = [ "'self'", + CHANGELOG_MEDIA_ORIGIN, 'https://api.browser-use.com', 'https://api.elevenlabs.io', 'wss://api.elevenlabs.io', @@ -215,7 +217,7 @@ const buildTimeCSPDirectives: CSPDirectives = { 'img-src': [...STATIC_IMG_SRC], - 'media-src': ["'self'", 'blob:'], + 'media-src': ["'self'", 'blob:', CHANGELOG_MEDIA_ORIGIN], 'worker-src': ["'self'", 'blob:'], 'font-src': ["'self'", 'https://fonts.gstatic.com'], diff --git a/apps/sim/lib/postcss/hairline-border-width.mjs b/apps/sim/lib/postcss/hairline-border-width.cjs similarity index 99% rename from apps/sim/lib/postcss/hairline-border-width.mjs rename to apps/sim/lib/postcss/hairline-border-width.cjs index 6d7329371cd..6aa92fa5444 100644 --- a/apps/sim/lib/postcss/hairline-border-width.mjs +++ b/apps/sim/lib/postcss/hairline-border-width.cjs @@ -105,4 +105,4 @@ const plugin = () => ({ }) plugin.postcss = true -export default plugin +module.exports = plugin diff --git a/apps/sim/lib/postcss/hairline-border-width.test.ts b/apps/sim/lib/postcss/hairline-border-width.test.ts index 3000cadf5fe..784f6cb68c2 100644 --- a/apps/sim/lib/postcss/hairline-border-width.test.ts +++ b/apps/sim/lib/postcss/hairline-border-width.test.ts @@ -5,7 +5,7 @@ */ import postcss from 'postcss' import { describe, expect, it } from 'vitest' -import hairline from '@/lib/postcss/hairline-border-width.mjs' +import hairline from '@/lib/postcss/hairline-border-width.cjs' const FROM = '/repo/apps/sim/app/_styles/globals.css' diff --git a/apps/sim/lib/workflows/comparison/overlay.ts b/apps/sim/lib/workflows/comparison/overlay.ts index 3ed1f922132..e4fd871342c 100644 --- a/apps/sim/lib/workflows/comparison/overlay.ts +++ b/apps/sim/lib/workflows/comparison/overlay.ts @@ -1,4 +1,4 @@ -import { BLOCK_DIMENSIONS, CONTAINER_DIMENSIONS } from '@sim/workflow-renderer' +import { BLOCK_DIMENSIONS, CONTAINER_DIMENSIONS } from '@sim/workflow-renderer/dimensions' import { collectErrorSourceBlockIds, normalizeWorkflowEdgeHandles, diff --git a/apps/sim/postcss.config.mjs b/apps/sim/postcss.config.mjs index ef674127bde..b7c3907a456 100644 --- a/apps/sim/postcss.config.mjs +++ b/apps/sim/postcss.config.mjs @@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url' * directory, so a project-relative specifier is not found there. */ const hairlineBorderWidth = fileURLToPath( - new URL('./lib/postcss/hairline-border-width.mjs', import.meta.url) + new URL('./lib/postcss/hairline-border-width.cjs', import.meta.url) ) /** @type {import('postcss-load-config').Config} */ diff --git a/apps/sim/proxy.ts b/apps/sim/proxy.ts index 2879c6804dc..94a11376773 100644 --- a/apps/sim/proxy.ts +++ b/apps/sim/proxy.ts @@ -460,7 +460,7 @@ export const config = { // Runtime CORS. The desktop's raw upload routes are left out: running the proxy makes Next // buffer the body, cutting it off at its 10 MB proxy limit, and those bodies are whole files. '/api/((?!desktop/tool/(?:import|file)$).*)', - // Catch-all for other pages, excluding static assets and public directories - '/((?!api/|api$|_next/static|_next/image|ingest|favicon.ico|logo/|landing/|static/|footer/|social/|enterprise/|favicon/|twitter/|robots.txt|sitemap.xml).*)', + // Next's image optimizer fetches public editorial images without a User-Agent. + '/((?!api/|api$|_next/static|_next/image|(?:blog|changelog)/.*\\.(?:avif|gif|jpe?g|png|svg|webp)$|ingest|favicon.ico|logo/|landing/|static/|footer/|social/|enterprise/|favicon/|twitter/|robots.txt|sitemap.xml).*)', ], } diff --git a/apps/sim/public/changelog/compare-workflow-deployments-v1.jpg b/apps/sim/public/changelog/compare-workflow-deployments-v1.jpg new file mode 100644 index 00000000000..23dc6633c7e Binary files /dev/null and b/apps/sim/public/changelog/compare-workflow-deployments-v1.jpg differ diff --git a/apps/sim/public/changelog/compare-workflow-deployments-v1.mp4 b/apps/sim/public/changelog/compare-workflow-deployments-v1.mp4 new file mode 100644 index 00000000000..aadc46bf236 Binary files /dev/null and b/apps/sim/public/changelog/compare-workflow-deployments-v1.mp4 differ diff --git a/apps/sim/public/changelog/power-bi-query-v1.jpg b/apps/sim/public/changelog/power-bi-query-v1.jpg new file mode 100644 index 00000000000..97059d4eb92 Binary files /dev/null and b/apps/sim/public/changelog/power-bi-query-v1.jpg differ diff --git a/apps/sim/public/changelog/review-fork-changes-v1.jpg b/apps/sim/public/changelog/review-fork-changes-v1.jpg new file mode 100644 index 00000000000..5630874d6f3 Binary files /dev/null and b/apps/sim/public/changelog/review-fork-changes-v1.jpg differ diff --git a/apps/sim/public/changelog/review-fork-changes-v1.mp4 b/apps/sim/public/changelog/review-fork-changes-v1.mp4 new file mode 100644 index 00000000000..c2f928bb1f4 Binary files /dev/null and b/apps/sim/public/changelog/review-fork-changes-v1.mp4 differ diff --git a/bun.lock b/bun.lock index 69ac6322289..a720c517ad3 100644 --- a/bun.lock +++ b/bun.lock @@ -10,6 +10,7 @@ "@mdx-js/mdx": "3.1.1", "@octokit/rest": "^21.0.0", "@sim/utils": "workspace:*", + "@types/mdast": "4.0.4", "@types/opentype.js": "1.3.10", "@typescript/native": "npm:typescript@^7.0.2", "@typescript/typescript6": "^6.0.2", diff --git a/package.json b/package.json index 603a3cfa0ff..cd13aa51d69 100644 --- a/package.json +++ b/package.json @@ -171,6 +171,7 @@ "@mdx-js/mdx": "3.1.1", "@octokit/rest": "^21.0.0", "@sim/utils": "workspace:*", + "@types/mdast": "4.0.4", "@types/opentype.js": "1.3.10", "@typescript/native": "npm:typescript@^7.0.2", "@typescript/typescript6": "^6.0.2", diff --git a/packages/emcn/src/hooks/use-scroll-edges.ts b/packages/emcn/src/hooks/use-scroll-edges.ts index 64ac6d87ca2..3f0744e719a 100644 --- a/packages/emcn/src/hooks/use-scroll-edges.ts +++ b/packages/emcn/src/hooks/use-scroll-edges.ts @@ -1,3 +1,5 @@ +'use client' + import { type RefObject, useEffect, useState } from 'react' import type { ScrollEdges, ScrollEdgesX } from '../components/scroll-fade/scroll-fade' diff --git a/scripts/check-library-content.test.ts b/scripts/check-library-content.test.ts index ce8d01c9649..efc85dc8eb4 100644 --- a/scripts/check-library-content.test.ts +++ b/scripts/check-library-content.test.ts @@ -25,12 +25,14 @@ function writePost( section: string, slug: string, body: string, - frontmatter: Record = {} + frontmatter: Record = {}, + includeMedia = true ) { const fields = { slug, ...CLEAN_FRONTMATTER, ogImage: `/${section}/${slug}/cover.jpg`, + ...(section === 'changelog' ? { ogAlt: 'A sample workflow result in Sim' } : {}), ...frontmatter, } const yaml = Object.entries(fields) @@ -38,7 +40,11 @@ function writePost( .join('\n') const dir = path.join(config.contentDir, section, slug) mkdirSync(dir, { recursive: true }) - writeFileSync(path.join(dir, 'index.mdx'), `---\n${yaml}\n---\n\n${body}\n`) + const media = + section === 'changelog' && includeMedia + ? `\n\n` + : '' + writeFileSync(path.join(dir, 'index.mdx'), `---\n${yaml}\n---\n\n${body}\n${media}`) const imageDir = path.join(config.publicDir, section, slug) mkdirSync(imageDir, { recursive: true }) writeFileSync(path.join(imageDir, 'cover.jpg'), '') @@ -54,10 +60,21 @@ beforeEach(() => { config = { contentDir: path.join(root, 'content'), publicDir: path.join(root, 'public'), - reservedSegments: { blog: new Set(['tags']), library: new Set(['tags']), customers: new Set() }, + reservedSegments: { + blog: new Set(['tags']), + library: new Set(['tags']), + customers: new Set(), + changelog: new Set(['archive', 'preview']), + }, mergedSlugs: { 'old-guide': 'kept-guide' }, movedBlogSlugs: ['moved-post'], customerSlugs: ['acme'], + linkedPages: new Set([ + 'https://www.sim.ai/integrations/you-com', + 'https://docs.sim.ai/workflows/deployment', + 'https://www.sim.ai/models/anthropic', + 'https://www.sim.ai/models/anthropic/claude-opus-4-6', + ]), } mkdirSync(path.join(config.contentDir, 'authors'), { recursive: true }) writeFileSync(path.join(config.contentDir, 'authors', 'sim.json'), '{"id":"sim","name":"Sim"}') @@ -70,6 +87,137 @@ afterEach(() => { }) describe('check-library-content', () => { + it.each([ + 'An update with only a cover image.', + '```mdx\n\n```', + ])('rejects a published changelog without rendered feature media: %s', async (body) => { + writePost('changelog', 'post', body, {}, false) + expect(await findingsFor('changelog', 'post')).toEqual([ + expect.objectContaining({ rule: 'media', message: expect.stringContaining('at least one') }), + ]) + writePost('changelog', 'post', body, { draft: 'true' }, false) + expect(await findingsFor('changelog', 'post')).toEqual([]) + }) + + it.each([ + { date: '2999-01-01T00:00:00Z' }, + { date: '2026-09-01T00:00:00Z', updated: '2026-08-01T00:00:00Z' }, + ])('rejects misleading publication dates in a changelog: %o', async (dates) => { + writePost('changelog', 'post', 'A product update.', dates) + expect(await findingsFor('changelog', 'post')).toEqual([ + expect.objectContaining({ rule: 'frontmatter', message: expect.stringContaining('date') }), + ]) + }) + + it('rejects two published updates with the same RSS identity', async () => { + const release = + '\n versions: [v0.9.10]\n url: https://github.com/simstudioai/sim/releases/tag/v0.9.10' + writePost('changelog', 'first-story', 'One capability.', { release }) + writePost('changelog', 'second-story', 'Another capability.', { release }) + const { findings } = await checkContent(config) + expect(findings).toEqual([ + expect.objectContaining({ + rule: 'frontmatter', + message: expect.stringContaining('RSS identity'), + }), + ]) + writePost('changelog', 'second-story', 'Another capability.', { release, draft: 'true' }) + expect((await checkContent(config)).findings).toEqual([]) + }) + + it.each([ + '[You.com](/integrations/youcom)', + '[Deployment guide](https://docs.sim.ai/workflows/deployments)', + '[Deployment guide][guide]\n\n[guide]: https://docs.sim.ai/workflows/deployments', + 'Deployment guide', + '[Model provider](/models/missing-provider)', + '[Model](https://www.sim.ai/models/anthropic/missing-model)', + '[Model][model]\n\n[model]: /models/anthropic/missing-model', + 'Model', + ])('rejects a changelog backlink to a missing page: %s', async (invalidLink) => { + writePost( + 'changelog', + 'post', + [ + '[You.com](/integrations/you-com)', + '[Deployment guide](https://docs.sim.ai/workflows/deployment?source=changelog#api)', + '[Anthropic](/models/anthropic/)', + '[Claude Opus](https://www.sim.ai/models/anthropic/claude-opus-4-6?source=changelog#capabilities)', + '```md', + '[Example](https://docs.sim.ai/workflows/missing-example)', + '```', + invalidLink, + ].join('\n\n') + ) + expect(await findingsFor('changelog', 'post')).toEqual([ + expect.objectContaining({ + rule: 'internal-link', + message: expect.stringContaining('does not exist'), + }), + ]) + }) + + it('rejects a changelog link to an unpublished draft', async () => { + writePost('changelog', 'draft-update', 'Draft.', { draft: 'true' }) + writePost('changelog', 'post', '[Next update](/changelog/draft-update)') + expect(await findingsFor('changelog', 'post')).toEqual([ + expect.objectContaining({ rule: 'internal-link', message: expect.stringContaining('draft') }), + ]) + }) + + it('rejects a changelog slug that shadows the archive route', async () => { + writePost('changelog', 'archive', 'Body.') + expect(await findingsFor('changelog', 'archive')).toEqual([ + expect.objectContaining({ rule: 'slug', message: expect.stringContaining('reserved') }), + ]) + }) + + it('rejects changelog body headings that collide with the page hierarchy', async () => { + writePost('changelog', 'post', '## Improvements\n\nA clearer comparison.') + expect(await findingsFor('changelog', 'post')).toEqual([ + expect.objectContaining({ rule: 'mdx', message: expect.stringContaining('heading') }), + ]) + }) + + it('accepts local media and the approved CDN and rejects a lookalike origin', async () => { + const origin = 'https://nnjgp7vypgx4myuq.public.blob.vercel-storage.com' + mkdirSync(path.join(config.publicDir, 'changelog/media'), { recursive: true }) + writeFileSync(path.join(config.publicDir, 'changelog/media/demo.mp4'), 'recording') + writeFileSync(path.join(config.publicDir, 'changelog/media/demo.vtt'), 'WEBVTT\n') + for (const [slug, src] of [ + ['good-video', `${origin}/changelog/demo-v1.mp4`], + ['local-video', '/changelog/media/demo.mp4'], + ['bad-video', `${origin}.example.com/changelog/demo-v1.mp4`], + ]) { + writePost( + 'changelog', + slug, + `` + ) + } + const { findings } = await checkContent(config) + expect(findings).toEqual([ + expect.objectContaining({ + file: path.join(config.contentDir, 'changelog/bad-video/index.mdx'), + rule: 'media', + }), + ]) + }) + + it.each([ + '', + '', + '', + '', + '', + '', + ])('rejects changelog media that cannot render or load: %s', async (body) => { + writePost('changelog', 'post', body) + expect(await findingsFor('changelog', 'post')).toEqual([ + expect.objectContaining({ rule: 'media' }), + ]) + }) + it('passes a clean post with valid internal links, assets, reserved routes, and code samples', async () => { writePost( 'library', diff --git a/scripts/check-library-content.ts b/scripts/check-library-content.ts index e59d8432d76..ea81e3786b5 100644 --- a/scripts/check-library-content.ts +++ b/scripts/check-library-content.ts @@ -1,6 +1,6 @@ #!/usr/bin/env bun /** - * Validates every content post under `apps/sim/content/{blog,library,customers}//index.mdx`. + * Validates every content post under `apps/sim/content/{blog,library,customers,changelog}//index.mdx`. * * Content is mostly written by agents and merged without a build, so a bad post only fails at * `next build` (an invalid frontmatter key, an MDX syntax error) or never fails at all (a link to a @@ -12,6 +12,11 @@ * - `slug`: the `slug` field equals the post's folder name. * - `og-image`: `ogImage` is a local path to a file under `apps/sim/public`. * - `mdx`: the body compiles with the MDX compiler and `remark-gfm`, as the registry compiles it. + * - `media`: changelog screenshots have literal dimensions and existing assets; recordings and + * captions use existing local assets or the approved CDN, matching the site's CSP. + * Published changelog bodies must contain feature media; a cover or fenced example is not enough. + * - Changelog publication dates cannot be in the future, correction dates cannot precede + * publication, and published updates cannot reuse the same RSS identity. * - `faq`: the body has no FAQ heading (the FAQ lives in frontmatter, which renders it and emits * its JSON-LD), and no FAQ question or answer contains Markdown link syntax (it renders as text). * - `internal-link`: every `https://www.sim.ai/
      /` link, and every relative @@ -19,25 +24,35 @@ * or a customer story registered in `CUSTOMER_STORIES` — never a retired or moved slug. Every * retired or moved slug redirects to a published library post. Apex `https://sim.ai` links * belong to `check:site-urls`. + * Changelog integration, model, and docs links also resolve against the generated catalogs, + * docs source files, and generated OpenAPI operation pages, including reference-style links. * * Run one post with `--slug
      /` or `--slug `. */ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs' import path from 'node:path' import { compile } from '@mdx-js/mdx' +import integrationsJson from '@sim/deployment-config/integrations.json' +import { SIM_DOCS_URL, SIM_SITE_URL } from '@sim/utils/site' import matter from 'gray-matter' +import type { Root } from 'mdast' import remarkGfm from 'remark-gfm' +import { visit } from 'unist-util-visit' +import { OPENAPI_SPEC_FILES } from '../apps/docs/lib/openapi-specs' +import { MODEL_PROVIDERS_WITH_MODELS } from '../apps/sim/app/(landing)/models/utils' +import { isChangelogMediaSource } from '../apps/sim/lib/changelog/media' import { AuthorSchema, ContentFrontmatterSchema } from '../apps/sim/lib/content/schema' import { CUSTOMER_STORIES } from '../apps/sim/lib/customers/data' import { LIBRARY_MERGED_SLUGS, LIBRARY_MOVED_BLOG_SLUGS, } from '../apps/sim/lib/library/retired-slugs' +import { foldDocsIndexPath } from '../apps/sim/lib/mothership/docs/docs-path' -export const SECTIONS = ['blog', 'library', 'customers'] as const +export const SECTIONS = ['blog', 'library', 'customers', 'changelog'] as const export type Section = (typeof SECTIONS)[number] -export type Rule = 'frontmatter' | 'slug' | 'og-image' | 'mdx' | 'faq' | 'internal-link' +export type Rule = 'frontmatter' | 'slug' | 'og-image' | 'mdx' | 'media' | 'faq' | 'internal-link' export interface Finding { file: string @@ -60,6 +75,8 @@ export interface ContentCheckConfig { movedBlogSlugs: readonly string[] /** Customer slugs the `/customers/[slug]` route serves (`CUSTOMER_STORIES`). */ customerSlugs: readonly string[] + /** Canonical integration, model, and documentation pages served by the current source tree. */ + linkedPages: ReadonlySet } /** Post folders per section, keyed by slug, with each post's `draft` flag. */ @@ -75,7 +92,7 @@ export interface PostRef { * `href` attribute. Group 1 is the section, group 2 the first segment, group 3 anything after it. */ const INTERNAL_LINK = - /(?:https?:\/\/www\.sim\.ai|(?<=\]\(\s*|href=\{?["'`]))\/(library|blog|customers)\/([^\s)"'`#?/<>\]]+)(\/[^\s)"'`#?<>\]]*)?/g + /(?:https?:\/\/www\.sim\.ai|(?<=\]\(\s*|href=\{?["'`]))\/(library|blog|customers|changelog)\/([^\s)"'`#?/<>\]]+)(\/[^\s)"'`#?<>\]]*)?/g /** Sentence punctuation that ends a bare URL in prose (`…see https://www.sim.ai/library/x.`). */ const TRAILING_PUNCTUATION = /[.,;:!]+$/ /** Text just before a Markdown link target or `href` value, whose URL ends at its delimiter. */ @@ -173,6 +190,134 @@ function lineOfKey(frontmatterLines: string[], key: string): number { return index === -1 ? 1 : index + 2 } +type ReportFinding = (line: number, rule: Rule, message: string, hint: string) => void + +function checkChangelogBody( + tree: Root, + config: ContentCheckConfig, + bodyOffset: number, + published: boolean, + report: ReportFinding +): void { + let mediaCount = 0 + const definitions = new Map() + visit(tree, 'definition', (node) => { + definitions.set(node.identifier, node.url) + }) + const checkLink = (href: string, line: number) => { + let url: URL + try { + url = new URL(href, SIM_SITE_URL) + } catch { + return + } + const isDocs = url.hostname === new URL(SIM_DOCS_URL).hostname + const isCatalog = + url.hostname === new URL(SIM_SITE_URL).hostname && + /^\/(?:integrations|models)(?:\/|$)/.test(url.pathname) + if (!isDocs && !isCatalog) return + const canonical = `${url.origin}${url.pathname.replace(/\/$/, '')}` + if (!config.linkedPages.has(canonical)) { + report( + line, + 'internal-link', + `${href} does not exist in the published integration/model catalogs or documentation source.`, + 'Use the canonical catalog or docs URL; keep feature and setup links relevant to the update.' + ) + } + } + visit(tree, (node) => { + const line = bodyOffset + (node.position?.start.line ?? 1) + if (node.type === 'link') checkLink(node.url, line) + if (node.type === 'linkReference') { + const href = definitions.get(node.identifier) + if (href) checkLink(href, line) + } + if (node.type === 'heading' && node.depth < 3) { + report( + bodyOffset + (node.position?.start.line ?? 1), + 'mdx', + 'Changelog body heading must start at level 3.', + 'Use ### for entry sections; the article title and details section own H1 and H2.' + ) + } + if (node.type !== 'mdxJsxFlowElement' && node.type !== 'mdxJsxTextElement') return + if (node.name === 'a' || node.name === 'Link') { + const href = node.attributes.find( + (attribute) => attribute.type === 'mdxJsxAttribute' && attribute.name === 'href' + ) + if (href?.type === 'mdxJsxAttribute' && typeof href.value === 'string') { + checkLink(href.value, line) + } + } + if (node.name !== 'ChangelogImage' && node.name !== 'ChangelogVideo') return + mediaCount += 1 + const name = node.name + const attributes = new Map( + node.attributes + .filter((attribute) => attribute.type === 'mdxJsxAttribute') + .map((attribute) => [attribute.name, attribute.value]) + ) + const literal = (field: string) => { + const value = attributes.get(field) + return typeof value === 'string' && value.trim() ? value : undefined + } + const fail = (field: string, reason: string) => + report( + line, + 'media', + `${name} ${field} ${reason}.`, + 'Use literal attributes, existing local images, and same-origin or approved CDN media. Verify hosted media in the deployment preview.' + ) + const asset = (field: string, localOnly: boolean) => { + const value = literal(field) + if (!value || !isChangelogMediaSource(value) || (localOnly && !value.startsWith('/'))) { + fail(field, 'must be a literal same-origin path or approved media CDN URL') + return + } + if (!value.startsWith('/')) return + const target = path.resolve(config.publicDir, `.${value}`) + const relative = path.relative(config.publicDir, target) + if (relative.startsWith('..') || path.isAbsolute(relative)) { + fail(field, 'resolves outside public/') + } else if (!existsSync(target) || !statSync(target).isFile()) { + fail(field, 'does not exist under public/') + } + } + asset('src', name === 'ChangelogImage') + if (name === 'ChangelogImage') { + if (!literal('alt')) fail('alt', 'must describe the screenshot') + for (const field of ['width', 'height']) { + const value = Number(literal(field)) + if (!Number.isInteger(value) || value <= 0) { + fail(field, 'must be a positive literal dimension; MDX strips JavaScript expressions') + } + } + } else { + asset('poster', true) + if (!literal('caption')) fail('caption', 'must describe the action and result') + const src = literal('src') + if (src && isChangelogMediaSource(src) && !src.endsWith('.mp4')) { + fail('src', 'must point to an MP4') + } + if (attributes.has('captionsSrc')) { + asset('captionsSrc', false) + if (!literal('captionsSrc')?.endsWith('.vtt')) { + fail('captionsSrc', 'must point to a WebVTT file') + } + } + } + }) + if (published && mediaCount === 0) { + report( + bodyOffset + 1, + 'media', + 'A published changelog needs at least one feature image or video in its body.', + 'Add a reviewed ChangelogImage or ChangelogVideo that demonstrates the update; a cover image or fenced example is not enough. Keep unfinished entries as drafts.' + ) + } +} + /** Validates one post, returning every finding (empty when the post is clean). */ export async function checkPost( config: ContentCheckConfig, @@ -235,6 +380,34 @@ export async function checkPost( const data = parsed.data as Record + if (section === 'changelog' && result.success) { + const { date, updated, draft, ogAlt } = result.data + if (!draft && (date.getTime() > Date.now() || (updated && updated.getTime() > Date.now()))) { + report( + lineOfKey(frontmatterLines, 'date'), + 'frontmatter', + 'A published changelog date cannot be in the future.', + 'Keep the entry as a draft until its actual publication date; deployment, not the date field, publishes content.' + ) + } + if (updated && updated < date) { + report( + lineOfKey(frontmatterLines, 'updated'), + 'frontmatter', + 'The updated date cannot precede the publication date.', + 'Preserve the original publication date and use the actual date of a substantive correction.' + ) + } + if (!draft && !ogAlt?.trim()) { + report( + lineOfKey(frontmatterLines, 'ogAlt'), + 'frontmatter', + 'A published changelog cover needs descriptive ogAlt text.', + 'Describe what the cover actually shows, rather than repeating the headline.' + ) + } + } + if (Array.isArray(data.authors)) { for (const author of data.authors) { if (typeof author === 'string' && !authorIds.has(author)) { @@ -257,6 +430,10 @@ export async function checkPost( ) } + if (config.reservedSegments[section].has(slug)) { + report(2, 'slug', `slug "${slug}" is reserved for a static route.`, 'Choose a different slug.') + } + if (typeof data.ogImage === 'string') { const line = lineOfKey(frontmatterLines, 'ogImage') if (!data.ogImage.startsWith('/')) { @@ -376,7 +553,18 @@ export async function checkPost( }) try { - await compile(body, { remarkPlugins: [remarkGfm], outputFormat: 'function-body' }) + await compile(body, { + remarkPlugins: [ + remarkGfm, + ...(section === 'changelog' + ? [ + () => (tree: Root) => + checkChangelogBody(tree, config, bodyOffset, data.draft !== true, report), + ] + : []), + ], + outputFormat: 'function-body', + }) } catch (error) { const { line, reason, message } = error as { line?: number; reason?: string; message?: string } report( @@ -457,7 +645,78 @@ export async function checkContent( const results = await Promise.all( targets.map((target) => checkPost(config, posts, authors.ids, target)) ) - return { checked: targets.length, findings: [...authors.findings, ...results.flat()] } + const findings = [...authors.findings, ...results.flat()] + const rssIdentities = new Map() + for (const [slug, post] of posts.changelog) { + if (post.draft) continue + const file = path.join(config.contentDir, 'changelog', slug, 'index.mdx') + let frontmatter: unknown + try { + frontmatter = matter(readFileSync(file, 'utf-8'), {}).data + } catch { + continue + } + const parsed = ContentFrontmatterSchema.safeParse(frontmatter) + if (!parsed.success || !parsed.data.release?.url) continue + const identity = parsed.data.release.url + const previous = rssIdentities.get(identity) + if (previous) { + findings.push({ + file, + line: 1, + rule: 'frontmatter', + message: `Changelog entries ${previous} and ${slug} share an RSS identity.`, + hint: 'Keep the release URL as the identity only for its migrated historical entry. For additional stories, omit release.url and link the release in the body.', + }) + } else { + rssIdentities.set(identity, slug) + } + } + return { checked: targets.length, findings } +} + +interface DocsOperation { + operationId?: string + tags?: string[] +} + +interface DocsSpec { + paths?: Record>> +} + +function readLinkedPages(docsAppDir: string): Set { + const pages = new Set([ + `${SIM_SITE_URL}/integrations`, + `${SIM_SITE_URL}/models`, + SIM_DOCS_URL, + ...integrationsJson.integrations.map( + (integration) => `${SIM_SITE_URL}/integrations/${integration.slug}` + ), + ...MODEL_PROVIDERS_WITH_MODELS.flatMap((provider) => [ + `${SIM_SITE_URL}${provider.href}`, + ...provider.models.map((model) => `${SIM_SITE_URL}${model.href}`), + ]), + ]) + const docsDir = path.join(docsAppDir, 'content/docs') + for (const file of readdirSync(docsDir, { recursive: true })) { + if (!file.endsWith('.mdx') || file === 'index.mdx') continue + const route = foldDocsIndexPath(file.split(path.sep).join('/')).replace(/\.mdx$/, '') + pages.add(`${SIM_DOCS_URL}/${route}`) + } + for (const file of OPENAPI_SPEC_FILES) { + const spec = JSON.parse(readFileSync(path.join(docsAppDir, file), 'utf8')) as DocsSpec + for (const item of Object.values(spec.paths ?? {})) { + for (const method of ['get', 'post', 'patch', 'delete', 'head', 'put']) { + const operation = item[method] + if (!operation?.operationId) continue + for (const tag of operation.tags?.length ? operation.tags : ['unknown']) { + const group = tag.replace(/\s+/g, '-').toLowerCase() + pages.add(`${SIM_DOCS_URL}/api-reference/${group}/${operation.operationId}`) + } + } + } + } + return pages } async function main() { @@ -472,6 +731,7 @@ async function main() { mergedSlugs: LIBRARY_MERGED_SLUGS, movedBlogSlugs: LIBRARY_MOVED_BLOG_SLUGS, customerSlugs: CUSTOMER_STORIES.map((story) => story.slug), + linkedPages: readLinkedPages(path.join(root, 'apps/docs')), } const args = process.argv.slice(2) diff --git a/scripts/check-unused-exports.baseline.json b/scripts/check-unused-exports.baseline.json index a5334669cea..23c07939cef 100644 --- a/scripts/check-unused-exports.baseline.json +++ b/scripts/check-unused-exports.baseline.json @@ -135,9 +135,6 @@ "apps/realtime/src/rooms/index.ts#RoomState", "apps/sim/app/(auth)/oauth/consent/consent-view.tsx#OAuthConsentRefusal", "apps/sim/app/(interfaces)/chat/components/message/message.tsx#ChatAttachment", - "apps/sim/app/(landing)/changelog/utils.ts#RELEASES_PER_PAGE", - "apps/sim/app/(landing)/changelog/utils.ts#extractMentions", - "apps/sim/app/(landing)/changelog/utils.ts#sanitizeContent", "apps/sim/app/(landing)/comparisons/comparison-sections.ts#ComparisonRowDef", "apps/sim/app/(landing)/comparisons/components/brand-icon-tile/index.ts#BrandIconTileProps", "apps/sim/app/(landing)/comparisons/components/brand-icon-tile/index.ts#SimIconTileProps",