├── tina ├── .gitignore ├── collections │ ├── page.ts │ ├── blog.ts │ └── global-config.ts ├── pages │ ├── Page.tsx │ ├── HomePage.tsx │ └── AdminBlogPost.tsx ├── config.ts ├── components │ └── IconComponent.tsx └── tina-lock.json ├── public ├── admin │ └── .gitignore ├── blog-placeholder-1.jpg ├── blog-placeholder-2.jpg ├── blog-placeholder-3.jpg ├── blog-placeholder-4.jpg ├── blog-placeholder-5.jpg ├── blog-placeholder-about.jpg ├── fonts │ ├── atkinson-bold.woff │ └── atkinson-regular.woff ├── favicon.svg └── llama.svg ├── .env.example ├── astro-tina-directive ├── index.d.ts ├── register.js └── tina.js ├── tsconfig.json ├── src ├── components │ ├── FormattedDate.astro │ ├── react │ │ ├── icon.tsx │ │ ├── FormattedDate.tsx │ │ └── IconLink.tsx │ ├── HeaderLink.astro │ ├── Header.astro │ ├── BaseHead.astro │ └── Footer.astro ├── pages │ ├── rss.xml.js │ ├── index.astro │ ├── blog │ │ ├── [...slug].astro │ │ └── index.astro │ └── [...slug].astro ├── content │ ├── config │ │ └── config.json │ ├── blog │ │ ├── using-mdx.mdx │ │ ├── second-post.mdx │ │ ├── first-post.mdx │ │ ├── third-post.mdx │ │ └── markdown-style-guide.mdx │ └── page │ │ ├── home.mdx │ │ └── about.mdx ├── layouts │ └── BlogPost.astro ├── content.config.ts └── styles │ └── global.css ├── astro.config.mjs ├── package.json ├── .gitignore └── README.md /tina/.gitignore: -------------------------------------------------------------------------------- 1 | __generated__ -------------------------------------------------------------------------------- /public/admin/.gitignore: -------------------------------------------------------------------------------- 1 | index.html 2 | assets/ -------------------------------------------------------------------------------- /public/blog-placeholder-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinacms/tina-astro-starter/HEAD/public/blog-placeholder-1.jpg -------------------------------------------------------------------------------- /public/blog-placeholder-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinacms/tina-astro-starter/HEAD/public/blog-placeholder-2.jpg -------------------------------------------------------------------------------- /public/blog-placeholder-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinacms/tina-astro-starter/HEAD/public/blog-placeholder-3.jpg -------------------------------------------------------------------------------- /public/blog-placeholder-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinacms/tina-astro-starter/HEAD/public/blog-placeholder-4.jpg -------------------------------------------------------------------------------- /public/blog-placeholder-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinacms/tina-astro-starter/HEAD/public/blog-placeholder-5.jpg -------------------------------------------------------------------------------- /public/blog-placeholder-about.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinacms/tina-astro-starter/HEAD/public/blog-placeholder-about.jpg -------------------------------------------------------------------------------- /public/fonts/atkinson-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinacms/tina-astro-starter/HEAD/public/fonts/atkinson-bold.woff -------------------------------------------------------------------------------- /public/fonts/atkinson-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinacms/tina-astro-starter/HEAD/public/fonts/atkinson-regular.woff -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | SITE_URL=https://example.com 2 | PUBLIC_TINA_CLIENT_ID=*** #Grab from TinaCloud app.tina.io 3 | TINA_TOKEN=*** #Grab from TinaCloud app.tina.io -------------------------------------------------------------------------------- /astro-tina-directive/index.d.ts: -------------------------------------------------------------------------------- 1 | import 'astro' 2 | declare module 'astro' { 3 | interface AstroClientDirectives { 4 | 'client:tina'?: boolean 5 | } 6 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "include": [".astro/types.d.ts", "**/*"], 4 | "exclude": ["dist"], 5 | "compilerOptions": { 6 | "strictNullChecks": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/components/FormattedDate.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | date: Date; 4 | } 5 | 6 | const { date } = Astro.props; 7 | --- 8 | 9 | 18 | -------------------------------------------------------------------------------- /astro-tina-directive/register.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {() => import('astro').AstroIntegration} 3 | */ 4 | export default () => ({ 5 | name: "client:tina", 6 | hooks: { 7 | "astro:config:setup": ({ addClientDirective }) => { 8 | addClientDirective({ 9 | name: "tina", 10 | entrypoint: "./astro-tina-directive/tina.js", 11 | }); 12 | }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /src/components/react/icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as Icons from "react-icons/tb"; 3 | 4 | type Props = { 5 | iconName: string; 6 | }; 7 | 8 | const ReactIcon = ({ iconName }: Props) => { 9 | const Icon = Icons[iconName as keyof typeof Icons]; 10 | 11 | if (!Icon) { 12 | return
Icon "{iconName}" not found
; 13 | } 14 | 15 | return ; 16 | }; 17 | 18 | export default ReactIcon; 19 | 20 | -------------------------------------------------------------------------------- /src/components/react/FormattedDate.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | 4 | function FormattedDate({ date }: { date: string }) { 5 | const _date = new Date(date) 6 | return ( 7 | 16 | ) 17 | } 18 | 19 | export default FormattedDate 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /astro.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { defineConfig } from 'astro/config'; 3 | import mdx from '@astrojs/mdx'; 4 | import sitemap from '@astrojs/sitemap'; 5 | import react from '@astrojs/react'; 6 | import tinaDirective from "./astro-tina-directive/register" 7 | 8 | // https://astro.build/config 9 | export default defineConfig({ 10 | site: process.env.SITE_URL || `https://${process.env.VERCEL_URL}`, 11 | integrations: [mdx(), sitemap(), react(), tinaDirective()], 12 | }); 13 | -------------------------------------------------------------------------------- /src/pages/rss.xml.js: -------------------------------------------------------------------------------- 1 | import rss from '@astrojs/rss'; 2 | import { getCollection } from 'astro:content'; 3 | import config from '../content/config/config.json' 4 | 5 | export async function GET(context) { 6 | const posts = await getCollection('blog'); 7 | return rss({ 8 | title: config.seo.title, 9 | description: config.seo.description, 10 | site: context.site, 11 | items: posts.map((post) => ({ 12 | ...post.data, 13 | link: `/blog/${post.id}/`, 14 | })), 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /astro-tina-directive/tina.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Hydrate on first click on the window 3 | * @type {import('astro').ClientDirective} 4 | */ 5 | export default async (load, options, el) => { 6 | try { 7 | const isInIframe = window.self !== window.top; 8 | if (!isInIframe) { 9 | return; 10 | } 11 | 12 | const hydrate = await load(); 13 | await hydrate(); 14 | } catch (error) { 15 | console.error("An error occurred in the Tina client directive:", error); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/react/IconLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactIcon from './icon' 3 | 4 | type LinkProps = { 5 | title: string; 6 | link: string; 7 | icon: string; 8 | } 9 | function IconLink({ title, link, icon }: LinkProps) { 10 | return ( 11 | 12 | {title} 13 | 14 | 15 | ) 16 | } 17 | 18 | export default IconLink 19 | -------------------------------------------------------------------------------- /tina/collections/page.ts: -------------------------------------------------------------------------------- 1 | import type { Collection } from "tinacms"; 2 | 3 | export const PageCollection: Collection = { 4 | name: "page", 5 | label: "Pages", 6 | path: "src/content/page", 7 | format: "mdx", 8 | ui: { 9 | router: ({ document }) => { 10 | return `/${document._sys.filename}`; 11 | }, 12 | }, 13 | fields: [ 14 | { 15 | name: "seoTitle", 16 | type: "string", 17 | required: true 18 | }, 19 | { 20 | name: "body", 21 | type: "rich-text", 22 | isBody: true, 23 | required: true 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/components/HeaderLink.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { HTMLAttributes } from 'astro/types'; 3 | 4 | type Props = HTMLAttributes<'a'>; 5 | 6 | const { href, class: className, ...props } = Astro.props; 7 | const pathname = Astro.url.pathname.replace(import.meta.env.BASE_URL, ''); 8 | const subpath = pathname.match(/[^\/]+/g); 9 | const isActive = href === pathname || href === '/' + (subpath?.[0] || ''); 10 | --- 11 | 12 | 13 | 14 | 15 | 25 | -------------------------------------------------------------------------------- /src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import BaseHead from '../components/BaseHead.astro'; 3 | import Header from '../components/Header.astro'; 4 | import Footer from '../components/Footer.astro'; 5 | import config from '../content/config/config.json'; 6 | import client from '../../tina/__generated__/client'; 7 | import HomePage from "../../tina/pages/HomePage.tsx"; 8 | const data = await client.queries.page({relativePath: "home.mdx"}); 9 | 10 | --- 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |