diff --git a/content/blog/Supercharge-Your-Markdown-Blog-with-AI.md b/content/blog/Supercharge-Your-Markdown-Blog-with-AI.md new file mode 100644 index 000000000..df61a8f12 --- /dev/null +++ b/content/blog/Supercharge-Your-Markdown-Blog-with-AI.md @@ -0,0 +1,43 @@ +--- +title: Markdown Bot - An AI friend who improves your content +date: '2023-07-14T04:00:00.000Z' +last_edited: '2023-07-14T04:00:00.000Z' +author: Logan Anderson +--- + +With [TinaCMS](https://tina.io), all your content changes are committed directly to Git. This enables your team to create a variety of workflows for reviewing and merging content updates. By leaning on GitHub, you can integrate CI/CD into your content workflow. + +To illustrate the potential of this combination, we're excited to introduce **Markdown Bot**, an AI friend who improves your content by making suggestions to your Pull Requests. + + + +> Want to skip the reading and jump straight to the code? [Check out the open source repo](https://github.com/tinacms/markdown-bot). + +## A Useful Aid, Not a Replacement + +AI can be a valuable tool for assisting with writing and editing content. We've designed this bot not to replace content editors, but rather to augment their capabilities. The bot offers content suggestions directly in your pull requests. If you find the suggestions helpful, you can commit them with a click. If not, they're just as easily dismissed. + +![AI Suggestion in Github](https://res.cloudinary.com/forestry-demo/image/upload/v1689957451/blog-media/markdown-bot/Screenshot_2023-07-21_at_12.36.48_PM_ztpdes.png "AI Suggestion in Github") + +## Markdown Bot Works On Your PRs + +There are many AI writing tools out there but if you use them with markdown content it often involved copying and pasting from AI outputs. We wanted something that could interact with our Content in GitHub. That's why we developed a GitHub bot that allows us to receive these suggestions right within a GitHub pull request. + +## Working with the GitHub Bot + +After you've integrated the bot into your repository, you can command it to make suggestions by commenting `ai fix: `. A custom prompt can be added by using `prompt: ` underneath. The bot will then offer commit suggestions in the form of a pull request review. + +To get started [check out the open source repo](https://github.com/tinacms/markdown-bot "AI Content Github repo"). + +## Looking Ahead: AI and Git-based Content + +Our GitHub bot works hand in hand with TinaCMS to enhance the content creation process. No longer do you need to manually copy and paste suggestions. The bot brings suggestions right to your pull requests for a smooth, efficient experience. + +We can envision some impressive custom workflows being built with AI and Git-based content. For instance, you could build off of this bot to: + +* Trigger the AI bot with custom events, such as opening a PR. +* Utilize analytics to suggest recommendations based on your top/bottom performing pages. +* Integrate this bot with your feedback widget, to open PRs based on user feedback. +* Catch insensitive, inconsiderate writing with tools like [alex](https://github.com/get-alex/alex) + +These are just a few of the many possibilities we see for integrating AI with Git-based content. We're excited about the potential here and look forward to seeing the creative workflows that the community will build. diff --git a/content/docs/contextual-editing/overview.md b/content/docs/contextual-editing/overview.md index 7c0edb6fd..3b9265f0f 100644 --- a/content/docs/contextual-editing/overview.md +++ b/content/docs/contextual-editing/overview.md @@ -16,3 +16,13 @@ Tina also allows for "Visual Editing" so that editors can see their pages being ## Adding contextual-editing to a page To add visual editing to a page, you'll need to hydrate the pages data. In React, this is done by [using the `useTina`](/docs/contextual-editing/react) hook. We are currently working on adding support for other frameworks such as [vue](/docs/contextual-editing/vue). + +## Video Tutorial + +For those who prefer to learn from video, you can check out a snippet on Visual Editing from our ["TinaCMS Deep Dive"](https://www.youtube.com/watch?v=PcgnJDILv4w&list=PLPar4H9PHKVqoCwZy79PHr8-W_vA3lAOB&pp=iAQB) series. + +
+ +
diff --git a/content/docs/editing/blocks.md b/content/docs/editing/blocks.md index c5d31fe6a..11bfb6d14 100644 --- a/content/docs/editing/blocks.md +++ b/content/docs/editing/blocks.md @@ -333,5 +333,12 @@ const featureBlock = { } ``` - - +## Video Tutorial + +For those who prefer to learn from video, you can check out a snippet on "Setting Up Blocks" from our ["TinaCMS Deep Dive"](https://www.youtube.com/watch?v=PcgnJDILv4w&list=PLPar4H9PHKVqoCwZy79PHr8-W_vA3lAOB&pp=iAQB) series. + +
+ +
diff --git a/content/docs/extending-tina/custom-field-components.md b/content/docs/extending-tina/custom-field-components.md index 78e65df43..b18267555 100644 --- a/content/docs/extending-tina/custom-field-components.md +++ b/content/docs/extending-tina/custom-field-components.md @@ -107,3 +107,13 @@ For example, if you take a look at the color field plugin's definition, it takes }, // ... ``` + +## Video Tutorial + +For those who prefer to learn from video, you can check out a snippet on "Customizing Components" from our ["TinaCMS Deep Dive"](https://www.youtube.com/watch?v=PcgnJDILv4w&list=PLPar4H9PHKVqoCwZy79PHr8-W_vA3lAOB&pp=iAQB) series. + +
+ +
diff --git a/content/docs/extending-tina/customize-list-ui.md b/content/docs/extending-tina/customize-list-ui.md index 218afc466..4f9a01fb2 100644 --- a/content/docs/extending-tina/customize-list-ui.md +++ b/content/docs/extending-tina/customize-list-ui.md @@ -85,3 +85,13 @@ For example: which will render as: ![List UI with label and style prop](https://res.cloudinary.com/forestry-demo/image/upload/v1649941182/tina-io/docs/extending-tina/Extending_Tina_Style_List_Props.png) + +## Video Tutorial + +For those who prefer to learn from video, you can check out a snippet on "Customizing List Items" from our ["TinaCMS Deep Dive"](https://www.youtube.com/watch?v=PcgnJDILv4w&list=PLPar4H9PHKVqoCwZy79PHr8-W_vA3lAOB&pp=iAQB) series. + +
+ +
diff --git a/content/docs/features/data-fetching.md b/content/docs/features/data-fetching.md index 685394820..618da0e0e 100644 --- a/content/docs/features/data-fetching.md +++ b/content/docs/features/data-fetching.md @@ -64,6 +64,16 @@ When developing locally, it's often beneficial to talk to the content on your lo > If you setup Tina via `@tinacms/cli init`, or used one of our starters, this should be setup by default. (Read about the CLI [here](/docs/graphql/cli/.) +## Video Tutorial + +For those who prefer to learn from video, you can check out a snippet on "Data Fetching" from our ["TinaCMS Deep Dive"](https://www.youtube.com/watch?v=PcgnJDILv4w&list=PLPar4H9PHKVqoCwZy79PHr8-W_vA3lAOB&pp=iAQB) series. + +
+ +
+ ## Summary - Tina provides a GraphQL API for querying your git-based content. diff --git a/content/docs/index.md b/content/docs/index.md index 25d609004..804bbb282 100644 --- a/content/docs/index.md +++ b/content/docs/index.md @@ -1,13 +1,13 @@ --- title: Tina Docs id: introduction -last_edited: '2021-07-27T15:51:56.737Z' +last_edited: '2023-07-18T15:51:56.737Z' next: /docs/product-tour --- ## Introduction -Tina is a Git-backed headless content management system that enables developers and content creators to collaborate seamlessly. With Tina, developers can create a custom visual editing experience that is perfectly tailored to their site. +Tina is an open-source, Git-backed headless content management system (CMS) that empowers both developers and content creators to collaborate seamlessly on a single platform. Tina enables developers to craft a custom visual editing experience perfectly tailored to their website or application while supporting a broad range of content types such as Markdown, MDX, and JSON.
+
diff --git a/content/docs/schema.md b/content/docs/schema.md index 07ee89f28..01009e3eb 100644 --- a/content/docs/schema.md +++ b/content/docs/schema.md @@ -285,6 +285,16 @@ Each field in a collection can be of the following `type`: - [object](/docs/reference/types/object/) - [rich-text](/docs/reference/types/rich-text/) +## Video Tutorial + +For those who prefer to learn from video, you can check out a snippet on media from our ["TinaCMS Deep Dive"](https://www.youtube.com/watch?v=PcgnJDILv4w&list=PLPar4H9PHKVqoCwZy79PHr8-W_vA3lAOB&pp=iAQB) series. + +
+ +
+ ## Summary - Your content is modeled in the `tina/config.{ts,js,tsx}` in your repo using `defineConfig`. diff --git a/content/docs/tina-cloud.md b/content/docs/tina-cloud.md index f0e31a2f0..c85bab4b2 100644 --- a/content/docs/tina-cloud.md +++ b/content/docs/tina-cloud.md @@ -31,3 +31,13 @@ To start moving from local-mode to prod-mode, the next steps are to: - Push your repository to GitHub (if it isn't already) - Set up a project in the Tina Cloud dashboard. (See next page) + +## Video Tutorial + +For those who prefer to learn from video, you can check out a snippet on "Tina Cloud" from our ["TinaCMS Deep Dive"](https://www.youtube.com/watch?v=PcgnJDILv4w&list=PLPar4H9PHKVqoCwZy79PHr8-W_vA3lAOB&pp=iAQB) series. + +
+ +
diff --git a/content/docs/tina-cloud/faq.md b/content/docs/tina-cloud/faq.md index 1431e8373..8e655d188 100644 --- a/content/docs/tina-cloud/faq.md +++ b/content/docs/tina-cloud/faq.md @@ -11,9 +11,9 @@ Tina Cloud adds a GraphQL API to Tina's open-source content editor allowing it t ## Where do I start? -- Have a look at the updated [Tina Cloud docs](/docs/setup-overview/) and try out a starter. -- [Sign up for Tina Cloud](https://app.tina.io/register)! -- [Find us on Discord](https://discord.com/invite/zumN63Ybpf) +* Have a look at the updated [Tina Cloud docs](/docs/setup-overview/) and try out a starter. +* [Sign up for Tina Cloud](https://app.tina.io/register)! +* [Find us on Discord](https://discord.com/invite/zumN63Ybpf) ## Does Tina Cloud only work with GitHub repositories? @@ -21,10 +21,10 @@ Currently, yes, the first Git provider that Tina Cloud integrates with is GitHub ## How can I share an idea or get help using Tina Cloud? -- If you haven't checked yet, the [docs](/docs/) may have the answer you are looking for! -- Connect with us on [Discord](https://discord.com/invite/zumN63Ybpf). -- We can help you at support@tina.io. Email us if you would like to schedule a chat! -- Chat with us from your Tina Cloud dashboard (there's a chat widget on the bottom right of the browser window). +* If you haven't checked yet, the [docs](/docs/) may have the answer you are looking for! +* Connect with us on [Discord](https://discord.com/invite/zumN63Ybpf). +* We can help you at support@tina.io. Email us if you would like to schedule a chat! +* Chat with us from your Tina Cloud dashboard (there's a chat widget on the bottom right of the browser window). ## What is the pricing for Tina Cloud? @@ -34,7 +34,7 @@ A fair use policy will be coming soon. We will contact you if we believe your use case may eventually fit within our post-beta paid plans. -## **Does Tina Cloud work with Monorepos?** +## Does Tina Cloud work with Monorepos? It does! Tina Cloud can work with sites inside monorepos by specifying the path to your `tina` folder in your Project configuration. @@ -54,12 +54,12 @@ See [Path To Tina](/docs/tina-cloud/dashboard/projects/#path-to-tina) for more i Tina Cloud's GraphQL API returns this error when it cannot find a file in your GitHub repository. This may occur under the following circumstances: -- The `tina` folder (and `__generated__` subfolder) is not in your GitHub repository remote. - - If the folder is in your local repository, but not in your remote, make sure there isn't a `.gitignore` file excluding it. -- Tina is configured with a branch that doesn't exist or a branch that doesn't contain the `tina` folder. - - The referenced branch should be created and should contain the `tina` folder. -- The apiURL prop is misconfigured on the TinaCMS component. - - Check the apiURL and make sure it looks like `https://content.tinajs.io/content/{tina_client_id}/github/{branch}` where `{tina_client_id}` matches the Client ID on the Project in Tina Cloud and `{branch}` is a valid branch. +* The `tina` folder (and `__generated__` subfolder) is not in your GitHub repository remote. + * If the folder is in your local repository, but not in your remote, make sure there isn't a `.gitignore` file excluding it. +* Tina is configured with a branch that doesn't exist or a branch that doesn't contain the `tina` folder. + * The referenced branch should be created and should contain the `tina` folder. +* The apiURL prop is misconfigured on the TinaCMS component. + * Check the apiURL and make sure it looks like `https://content.tinajs.io/content/{tina_client_id}/github/{branch}` where `{tina_client_id}` matches the Client ID on the Project in Tina Cloud and `{branch}` is a valid branch. ## Tina.io login window doesn't close when logging in from a site @@ -67,12 +67,18 @@ When a user logs in from your site, we will pop open a login window. When login The most common reasons for this issue are: -- The Site URL is not properly set for the Tina project. The main window's base URL will need to match the Tina project's Site URL setup in the Tina Cloud Dashboard. -- The Client ID setup in your site's environment variables does not match the Client ID in your project's settings on the Tina Cloud dashboard. -- The user attempting to login to Tina Cloud does not have access to edit this site. Ensure that this user is authorized on the Tina Cloud dashboard. +* The Site URL is not properly set for the Tina project. The main window's base URL will need to match the Tina project's Site URL setup in the Tina Cloud Dashboard. +* The Client ID setup in your site's environment variables does not match the Client ID in your project's settings on the Tina Cloud dashboard. +* The user attempting to login to Tina Cloud does not have access to edit this site. Ensure that this user is authorized on the Tina Cloud dashboard. > Make sure to include `https` in the Site URL eg: https://forestry.io or if you are testing locally, it might be something like `http://localhost:3000` +## How do I resolve "The local GraphQL schema doesn't match the remote GraphQL schema." errors? + +If you are getting this error in your build logs, it means that the tina/tina-lock.json in your deployed site doesn't match the version that is in Tina Cloud. To resolve it, make sure you have latest versions of @tinacms/cli and tinacms in your project, and then run the dev command locally. Commit any changes to the tina/tina-lock.json and push those to the git repository linked in Tina Cloud. + +If you are getting this error when access the TinaCMS interface, it can be caused by a mismatch between the version of tinacms and @tinacms/cli on the project. Update both dependencies to the latest versions and run the dev command locally. Commit any changes to the tina/tina-lock.json and push those to the git repository linked in Tina Cloud. + ## How do I resolve errors caused by unindexed branches? If you receive an error like `The specified branch, 'my-branch-name', has not been indexed by Tina Cloud`, first verify that the correct branch has been specified in diff --git a/content/toc-doc.json b/content/toc-doc.json index bd55474c8..20f747207 100644 --- a/content/toc-doc.json +++ b/content/toc-doc.json @@ -9,7 +9,7 @@ }, { "slug": "/docs/product-tour/", - "title": "Product Tour" + "title": "Introduction To TinaCMS" }, { "title": "Getting Started", diff --git a/pages/docs/[...slug].tsx b/pages/docs/[...slug].tsx index 3031f6709..0eb47c3d8 100644 --- a/pages/docs/[...slug].tsx +++ b/pages/docs/[...slug].tsx @@ -132,7 +132,10 @@ export const getStaticPaths: GetStaticPaths = async function () { return { fallback: false, paths: files - .filter((file) => !file.endsWith('index.md')) + .filter( + (file) => + !file.endsWith('index.md') && !file.endsWith('product-tour.md') + ) .map((file) => { const path = file.substring(contentDir.length, file.length - 3) return { params: { slug: path.split('/') } } diff --git a/pages/docs/product-tour.tsx b/pages/docs/product-tour.tsx new file mode 100644 index 000000000..dcbfb37ff --- /dev/null +++ b/pages/docs/product-tour.tsx @@ -0,0 +1,271 @@ +import { getDocProps } from 'utils/docs/getDocProps' +import { GetStaticProps } from 'next' +import { useRouter } from 'next/router' +import { useTocListener } from 'utils/toc_helpers' +import React, { useCallback, useEffect, useMemo, useRef } from 'react' +import * as ga from '../../utils/ga' +import { NextSeo } from 'next-seo' +import { openGraphImage } from 'utils/open-graph-image' +import { DocsLayout, MarkdownContent } from 'components/layout' +import { + DocGridContent, + DocGridHeader, + DocGridToc, + DocsGrid, + DocsPageTitle, +} from './[...slug]' +import { Breadcrumbs } from 'components/DocumentationNavigation/Breadcrumbs' +import Toc from 'components/toc' +import { DocsPagination, LastEdited } from 'components/ui' +import styled from 'styled-components' + +export const getStaticProps: GetStaticProps = async function (props) { + return await getDocProps(props, 'product-tour') +} + +export default function Page(props) { + const router = useRouter() + + const data = props.file.data + + const isCloudDocs = router.asPath.includes('tina-cloud') + + const isBrowser = typeof window !== `undefined` + + const frontmatter = data.frontmatter + const markdownBody = data.markdownBody + const excerpt = props.file.data.excerpt + const tocItems = props.tocItems + + const { activeIds: _activeIds, contentRef } = useTocListener(data) + const activeIds = _activeIds.filter((id) => !!id) + const activeImg = useRef(null) + const transitionImg = useRef(null) + + useEffect(() => { + let imgTransitionTimeout: NodeJS.Timeout + if (typeof window === 'undefined') return + if (!activeIds.length) { + return + } + const imageSrc = ( + document.querySelector( + `h2#${activeIds[0]} ~ *:has(img) img, ` + + `h3#${activeIds[0]} ~ *:has(img) img, ` + + `h4#${activeIds[0]} ~ *:has(img) img` + ) as any + )?.src + + // limit activeIds to 1 + const deepestActiveIds = activeIds.slice(0, 1) + document.querySelectorAll('.focused').forEach((el) => { + if (deepestActiveIds.indexOf(el.id) === -1) { + el.classList.remove('focused') + } + }) + + deepestActiveIds.forEach((id) => { + const el = document.querySelector(`#${id}`) + if (el) { + el.classList.add('focused') + } + }) + + if (activeImg.current.src === imageSrc) return + + if (!activeImg.current.src) { + activeImg.current.src = imageSrc + } else { + transitionImg.current.src = imageSrc + transitionImg.current.style.opacity = '1' + activeImg.current.style.opacity = '0' + + imgTransitionTimeout = setTimeout(function () { + activeImg.current.src = imageSrc + transitionImg.current.style.opacity = '0' + activeImg.current.style.opacity = '1' + }, 350) + } + + return () => { + if (imgTransitionTimeout) { + activeImg.current.src = imageSrc + transitionImg.current.style.opacity = '0' + activeImg.current.style.opacity = '1' + + clearTimeout(imgTransitionTimeout) + } + } + }, [activeIds, transitionImg, activeImg]) + + React.useEffect(() => { + const handleRouteChange = (url) => { + ga.pageview(url) + } + //When the component is mounted, subscribe to router changes + //and log those page views + router.events.on('routeChangeComplete', handleRouteChange) + + // If the component is unmounted, unsubscribe + // from the event with the `off` method + return () => { + router.events.off('routeChangeComplete', handleRouteChange) + } + }, [router.events]) + return ( + <> + + + + + + {frontmatter.title} + + {/* + + */} + +
+ +
+ +
+
+
+ + +
+
+
+ + {(props.prevPage?.slug !== null || + props.nextPage?.slug !== null) && ( + + )} +
+
+
+ + ) +} + +export const DocContainer = styled.div` + display: block; + width: 100%; + position: relative; + padding: 1rem 2rem 3rem 2rem; + margin: 0 auto; +` + +const MAX_SPLIT_IMG_WIDTH = 768 +const SplitContent = styled.div` + display: flex; + position: relative; + + h3 { + font-size: 1.2rem; + } + + > * { + flex: 1; + margin: 0 10px; + padding: 10px; + box-sizing: border-box; + } + + @media (min-width: ${MAX_SPLIT_IMG_WIDTH + 1}px) { + #main-content-container > h3:not(:first-child), + #main-content-container > h2:not(:first-child) { + margin-top: 4.5rem !important; + } + + ul { + list-style: none; + } + + h2, + h3, + h4 { + color: var(--color-light-dark); + + &:not(.focused) * { + color: var(--color-light-dark); + } + + + p, + + ul { + padding-left: 1rem; + border-left: 4px solid var(--color-light-dark); + + color: var(--color-light-dark); + * { + color: var(--color-light-dark); + } + } + + &.focused { + color: var(--color-orange); + + + p, + + ul { + border-left: 4px solid var(--color-orange); + + color: var(--color-primary); + * { + color: var(--color-primary); + } + } + } + } + } + + #main-content-container img { + display: none; + } + + @media (max-width: ${MAX_SPLIT_IMG_WIDTH}px) { + #sticky-img-container { + display: none; + } + + #main-content-container img { + display: initial; + } + } + + #sticky-img-container { + position: sticky; + top: 10px; + width: 100%; + height: fit-content; + + img { + max-width: 100%; + max-height: calc(100vh - 100px); + position: absolute; + left: 50%; + transform: translate(-50%, 0%); + top: 0; + transition: opacity 0.35s ease-in-out; + } + } + + .img-container { + position: relative; + } +` diff --git a/utils/toc_helpers.ts b/utils/toc_helpers.ts index 278e376ff..7088e21bc 100644 --- a/utils/toc_helpers.ts +++ b/utils/toc_helpers.ts @@ -14,7 +14,7 @@ function createHeadings( 'h1, h2, h3, h4, h5, h6' ) - htmlElements.forEach(function(heading: HTMLHeadingElement) { + htmlElements.forEach(function (heading: HTMLHeadingElement) { headings.push({ id: heading.id, offset: heading.offsetTop, @@ -36,7 +36,7 @@ export function createTocListener( const throttledScroll = () => { const scrollPos = window.scrollY const newActiveIds = [] - const activeHeadingCandidates = headings.filter(heading => { + const activeHeadingCandidates = headings.filter((heading) => { return heading.offset - scrollPos < BASE_OFFSET }) @@ -51,7 +51,7 @@ export function createTocListener( if (activeHeading.level != 'H2') { const activeHeadingParentCandidates = activeHeadingCandidates.length > 0 - ? activeHeadingCandidates.filter(heading => { + ? activeHeadingCandidates.filter((heading) => { return heading.level == 'H2' }) : [] @@ -72,7 +72,7 @@ export function createTocListener( return function onScroll(): void { if (!tick) { - setTimeout(function() { + setTimeout(function () { throttledScroll() tick = false }, THROTTLE_INTERVAL) @@ -81,20 +81,60 @@ export function createTocListener( } } +function useHookWithRefCallback() { + const ref = React.useRef(null) + const setRef = React.useCallback((node) => { + if (ref.current) { + // Make sure to cleanup any events/references added to the last instance + } + + if (node) { + // Check if a node is actually passed. Otherwise node would be null. + // You can now do what you need to, addEventListeners, measure, etc. + } + + // Save a reference to the node + ref.current = node + }, []) + + return [setRef, ref] +} + +function useWindowSize() { + if (typeof window !== 'undefined') { + return { width: 1200, height: 800 } + } + + const [windowSize, setWindowSize] = React.useState<{ + width: number + height: number + }>() + + React.useEffect(() => { + window.addEventListener('resize', () => { + setWindowSize({ width: window.innerWidth, height: window.innerHeight }) + }) + }, []) + + return windowSize +} + export function useTocListener(data) { const [activeIds, setActiveIds] = React.useState([]) - const contentRef = React.useRef(null) + const [setRef, ref] = useHookWithRefCallback() + + const windowSize = useWindowSize() React.useEffect(() => { - if (typeof window === `undefined` || !contentRef.current) { + if (typeof window === `undefined` || !(ref as any).current) { return } - const activeTocListener = createTocListener(contentRef, setActiveIds) + const activeTocListener = createTocListener(ref as any, setActiveIds) window.addEventListener('scroll', activeTocListener) return () => window.removeEventListener('scroll', activeTocListener) - }, [contentRef, data]) + }, [(ref as any).current, data, windowSize]) - return { contentRef, activeIds } + return { contentRef: setRef, activeIds } }