├── .env.example ├── .github └── dependabot.yml ├── .gitignore ├── .graphqlrc.yml ├── .prettierrc.cjs ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── astro.config.mjs ├── package.json ├── pnpm-lock.yaml ├── public └── favicon.svg ├── src ├── components │ ├── AddToCartForm.svelte │ ├── AnnouncementBar.astro │ ├── CartDrawer.svelte │ ├── CartIcon.svelte │ ├── Footer.astro │ ├── Header.astro │ ├── Logo.astro │ ├── Money.svelte │ ├── ProductAccordions.astro │ ├── ProductBreadcrumb.astro │ ├── ProductCard.astro │ ├── ProductImageGallery.astro │ ├── ProductInformations.astro │ ├── ProductRecommendations.astro │ ├── ProductReviews.astro │ ├── Products.astro │ └── ShopifyImage.svelte ├── env.d.ts ├── layouts │ ├── BaseLayout.astro │ └── NotFoundLayout.astro ├── pages │ ├── 404.astro │ ├── index.astro │ └── products │ │ └── [...handle].astro ├── stores │ └── cart.ts ├── styles │ └── global.css └── utils │ ├── cache.ts │ ├── click-outside.ts │ ├── config.ts │ ├── graphql.ts │ ├── schemas.ts │ └── shopify.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | PUBLIC_SHOPIFY_SHOP={your-store}.myshopify.com 2 | PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 3 | PRIVATE_SHOPIFY_STOREFRONT_ACCESS_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | reviewers: 6 | - 'thomasKn' 7 | schedule: 8 | interval: 'weekly' 9 | time: '05:30' 10 | timezone: 'America/New_York' 11 | groups: 12 | patch-minor: 13 | applies-to: version-updates 14 | update-types: 15 | - 'minor' 16 | - 'patch' 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | .vercel/ 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /.graphqlrc.yml: -------------------------------------------------------------------------------- 1 | schema: node_modules/@shopify/hydrogen-react/storefront.schema.json 2 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ["prettier-plugin-astro", "prettier-plugin-tailwindcss"], 3 | overrides: [ 4 | { 5 | files: "*.astro", 6 | options: { 7 | parser: "astro", 8 | }, 9 | }, 10 | ], 11 | }; 12 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.validate.enable": true 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Thomas Cristina de Carvalho 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Astro starter theme to build a headless ecommerce website with Shopify 2 | 3 | The theme is built with Svelte but you can use any framework you like (React, Vue, Solid etc.) thanks to Astro. 4 | Tailwind UI free components are used for the design. 5 | 6 | ![astro_shopify_preview](https://user-images.githubusercontent.com/10447155/214480671-8380f410-cbfb-4f53-a6bb-5c744073e2f2.jpg) 7 | 8 | ## 🧑‍🚀 Where to start 9 | 10 | 1. Create a `.env` file based on `.env.example` with your Shopify store url and your public and private access tokens 11 | 2. The credentials are used inside the `/utils/config.ts` file, you can update the API version there 12 | 3. Run `npm install` or `yarn` or `pnpm install` 13 | 4. Run `npm run dev` or `yarn run dev` or `pnpm run dev` 14 | 15 | ## Shopify Configuration Guide 16 | 17 | - Create a new account or use an existing one. https://accounts.shopify.com/store-login 18 | - Add the [Shopify Headless channel](https://apps.shopify.com/headless) to your store 19 | - Click on `Add Storefront` 20 | - Copy/Paste your `public` and `private` access tokens to your .env file 21 | - Next, check Storefront API access scopes 22 | - `unauthenticated_read_product_listings` and `unauthenticated_read_product_inventory` access should be fine to get you started. 23 | - Add more scopes if you require additional permissions. 24 | 25 | ### Shopify Troubleshooting 26 | 27 | - If you encounter an error like `error code 401` you likely didn't set this up correctly. Revisit your scopes and be sure add at least one test product. Also make sure you are using the `Storefront API` and not the `Admin API` as the endpoints and scopes are different. 28 | - If you do not see a checkout sidebar, or if it is empty after adding a product, you need to add an image to your test product. 29 | 30 | ## 🚀 Project Structure 31 | 32 | Inside the project, you'll see the following folders and files: 33 | 34 | ``` 35 | / 36 | ├── public/ 37 | ├── src/ 38 | │ └── components/ 39 | │ └── Header.astro 40 | │ └── layouts/ 41 | │ └── BaseLayout.astro 42 | │ └── pages/ 43 | │ └── index.astro 44 | │ └── stores/ 45 | │ └── cart.ts 46 | │ └── styles/ 47 | │ └── global.css 48 | │ └── utils/ 49 | │ └── shopify.ts 50 | └── package.json 51 | ``` 52 | 53 | Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. 54 | 55 | There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. 56 | 57 | Any static assets, like images, can be placed in the `public/` directory. 58 | 59 | ## 🧞 Commands 60 | 61 | All commands are run from the root of the project, from a terminal: 62 | 63 | | Command | Action | 64 | | :--------------------- | :----------------------------------------------- | 65 | | `npm install` | Installs dependencies | 66 | | `npm run dev` | Starts local dev server at `localhost:3000` | 67 | | `npm run build` | Build your production site to `./dist/` | 68 | | `npm run preview` | Preview your build locally, before deploying | 69 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | 70 | | `npm run astro --help` | Get help using the Astro CLI | 71 | 72 | ## ⚡️ Lighthouse 73 | ![lighthouse_astro_shopify](https://user-images.githubusercontent.com/10447155/214448698-ce2a1ef6-6fbd-4fca-b8b6-c5194b72a15b.jpg) 74 | -------------------------------------------------------------------------------- /astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "astro/config"; 2 | import vercel from "@astrojs/vercel"; 3 | import tailwindcss from "@tailwindcss/vite"; 4 | 5 | // https://astro.build/config 6 | import svelte from "@astrojs/svelte"; 7 | 8 | // https://astro.build/config 9 | export default defineConfig({ 10 | output: "server", 11 | adapter: vercel(), 12 | 13 | integrations: [svelte()], 14 | 15 | vite: { 16 | plugins: [tailwindcss()], 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@example/minimal", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "private": true, 6 | "scripts": { 7 | "dev": "astro dev", 8 | "start": "astro dev", 9 | "build": "astro build", 10 | "preview": "astro preview", 11 | "astro": "astro", 12 | "typecheck": "tsc --noEmit" 13 | }, 14 | "dependencies": { 15 | "@astrojs/svelte": "^7.1.0", 16 | "@astrojs/vercel": "^8.1.5", 17 | "@nanostores/persistent": "^1.0.0", 18 | "@shopify/hydrogen-react": "^2025.5.0", 19 | "@tailwindcss/vite": "^4.1.8", 20 | "astro": "^5.9.1", 21 | "nanostores": "^1.0.1", 22 | "svelte": "^5.33.18", 23 | "zod": "^3.25.56" 24 | }, 25 | "devDependencies": { 26 | "prettier": "^3.5.3", 27 | "prettier-plugin-astro": "^0.14.1", 28 | "prettier-plugin-tailwindcss": "^0.6.12", 29 | "tailwindcss": "^4.1.8", 30 | "typescript": "^5.8.3" 31 | }, 32 | "engines": { 33 | "node": ">=20" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/AddToCartForm.svelte: -------------------------------------------------------------------------------- 1 | 32 | 33 |
addToCart(e))}> 34 | 35 | 36 | 37 | 70 | {#if noQuantityLeft} 71 |
72 | All units left are in your cart 73 |
74 | {/if} 75 |
76 | -------------------------------------------------------------------------------- /src/components/AnnouncementBar.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const message = "🎄 Free delivery for Christmas 🎁"; 3 | --- 4 | 5 |
8 | {message} 9 |
10 | -------------------------------------------------------------------------------- /src/components/CartDrawer.svelte: -------------------------------------------------------------------------------- 1 | 45 | 46 | {#if $isCartDrawerOpen} 47 | 241 | {/if} 242 | -------------------------------------------------------------------------------- /src/components/CartIcon.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 | 41 |
42 | -------------------------------------------------------------------------------- /src/components/Footer.astro: -------------------------------------------------------------------------------- 1 | 165 | -------------------------------------------------------------------------------- /src/components/Header.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Logo from "./Logo.astro"; 3 | import CartIcon from "./CartIcon.svelte"; 4 | --- 5 | 6 |
7 |
8 | 22 |
23 |
24 | -------------------------------------------------------------------------------- /src/components/Logo.astro: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 17 | 21 | 22 | 23 | 27 | 31 | 35 | 39 | 43 | 47 | 51 | 55 | 59 | 63 | 67 | 71 | 75 | 79 | 84 | 89 | 90 | 94 | 98 | 102 | 106 | 110 | 114 | 115 | 116 | 120 | 124 | 128 | 132 | 136 | 141 | 146 | 147 | 148 | 152 | 156 | 157 | 158 | 159 | 165 | 171 | 177 | 178 | 179 | 180 | 188 | 189 | 190 | 191 | 199 | 200 | 201 | 202 | 210 | 211 | 212 | 213 | 221 | 222 | 223 | 224 | 232 | 233 | 234 | 235 | 243 | 244 | 245 | 246 | 254 | 255 | 256 | 257 | 265 | 266 | 267 | 268 | 276 | 277 | 278 | 279 | 287 | 288 | 289 | 290 | 298 | 299 | 300 | 301 | 309 | 310 | 311 | 312 | 320 | 321 | 322 | 323 | 324 | 325 | 333 | 334 | 335 | 336 | 337 | 338 | 346 | 347 | 348 | 349 | 357 | 358 | 359 | 360 | 368 | 369 | 370 | 371 | 379 | 380 | 381 | 382 | 390 | 391 | 392 | 393 | 401 | 402 | 403 | 404 | 412 | 413 | 414 | 415 | 423 | 424 | 425 | 426 | 434 | 435 | 436 | 437 | 438 | 439 | 447 | 448 | 449 | 450 | 451 | 452 | 460 | 461 | 462 | 463 | 471 | 472 | 473 | 474 | 482 | 483 | 484 | 485 | 493 | 494 | 495 | 496 | 497 | 498 | -------------------------------------------------------------------------------- /src/components/Money.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | {formatPrice} 21 | 22 | -------------------------------------------------------------------------------- /src/components/ProductAccordions.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const accordions = [ 3 | { 4 | title: "Shipping", 5 | icon: "truck", 6 | content: 7 | "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ullam possimus fuga dolor rerum dicta, ipsum laboriosam est totam iusto alias incidunt cum tempore aliquid aliquam error quisquam ipsam asperiores! Iste?", 8 | }, 9 | { 10 | title: "Care instructions", 11 | icon: "care", 12 | content: 13 | "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ullam possimus fuga dolor rerum dicta, ipsum laboriosam est totam iusto alias incidunt cum tempore aliquid aliquam error quisquam ipsam asperiores! Iste?", 14 | }, 15 | ]; 16 | --- 17 | 18 |
19 | { 20 | accordions.map((accordion, index) => ( 21 |
28 | 29 |
30 | {accordion.icon === "truck" && ( 31 | 39 | 44 | 45 | )} 46 | {accordion.icon === "care" && ( 47 | 55 | 60 | 61 | )} 62 | {accordion.title} 63 |
64 | 65 | 73 | 78 | 79 | 80 |
81 |
{accordion.content}
82 |
83 | )) 84 | } 85 |
86 | -------------------------------------------------------------------------------- /src/components/ProductBreadcrumb.astro: -------------------------------------------------------------------------------- 1 | --- 2 | export interface Props { 3 | title: string; 4 | } 5 | const { title } = Astro.props as Props; 6 | --- 7 | 8 | 11 | 53 | -------------------------------------------------------------------------------- /src/components/ProductCard.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { z } from "zod"; 3 | import type { ProductResult } from "../utils/schemas"; 4 | 5 | import ShopifyImage from "./ShopifyImage.svelte"; 6 | import Money from "./Money.svelte"; 7 | 8 | export interface Props { 9 | product: z.infer; 10 | } 11 | const { product } = Astro.props as Props; 12 | --- 13 | 14 | 18 |
19 | 32 |
35 | 54 |
55 |
56 |
57 |

{product?.title}

58 |

59 | 60 |

61 |
62 |
63 | -------------------------------------------------------------------------------- /src/components/ProductImageGallery.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { z } from "zod"; 3 | import { ImageResult } from "../utils/schemas"; 4 | import ShopifyImage from "./ShopifyImage.svelte"; 5 | 6 | const ImagesResult = z.object({ 7 | nodes: z.array(ImageResult), 8 | }); 9 | 10 | export interface Props { 11 | images: z.infer; 12 | } 13 | const { images } = Astro.props as Props; 14 | --- 15 | 16 |
17 |
18 | 31 |
32 |
2 }, 43 | ]} 44 | > 45 | { 46 | images.nodes.map((image, index) => { 47 | if (index < 3) { 48 | return ( 49 |
50 | 60 |
61 | ); 62 | } 63 | }) 64 | } 65 |
66 |
67 | -------------------------------------------------------------------------------- /src/components/ProductInformations.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { z } from "zod"; 3 | import type { MoneyV2Result } from "../utils/schemas"; 4 | import Money from "./Money.svelte"; 5 | 6 | export interface Props { 7 | price?: z.infer; 8 | title: string; 9 | } 10 | const { price, title } = Astro.props as Props; 11 | --- 12 | 13 |

14 | {title} 15 |

16 |

17 | 18 |

19 | 20 | 21 |
22 |
23 |
24 | 30 | 33 | 34 | 40 | 43 | 44 | 50 | 53 | 54 | 60 | 63 | 64 | 70 | 73 | 74 |
75 | 42 Reviews 78 |
79 |
80 |

81 | This store is for demo purposes only. No orders will be fulfilled. Please 82 | visit the brand's website to purchase this product. 88 |

89 |
90 |
91 | -------------------------------------------------------------------------------- /src/components/ProductRecommendations.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getProductRecommendations } from "../utils/shopify"; 3 | import ProductCard from "./ProductCard.astro"; 4 | export interface Props { 5 | productId: string; 6 | buyerIP: string; 7 | } 8 | 9 | const { productId, buyerIP } = Astro.props as Props; 10 | 11 | const productRecommendations = await getProductRecommendations({ 12 | productId, 13 | buyerIP, 14 | }); 15 | --- 16 | 17 | { 18 | productRecommendations.length > 0 && ( 19 |
20 |
21 |

22 | Customers also purchased 23 |

24 | 25 |
26 | {productRecommendations.map((product) => ( 27 | 28 | ))} 29 |
30 |
31 |
32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/components/ProductReviews.astro: -------------------------------------------------------------------------------- 1 | 4 |
5 |

Customer Reviews

6 | 7 |
8 |

9 | 3.8 10 | Average review score 11 |

12 | 13 |
14 |
15 | 21 | 24 | 25 | 31 | 34 | 35 | 41 | 44 | 45 | 51 | 54 | 55 | 61 | 64 | 65 |
66 | 67 |

Based on 42 reviews

68 |
69 |
70 | 71 |
72 |
73 |
74 |
75 | 81 | 84 | 85 | 91 | 94 | 95 | 101 | 104 | 105 | 111 | 114 | 115 | 121 | 124 | 125 |
126 | 127 |

The best thing money can buy!

128 |
129 | 130 |

131 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ullam possimus 132 | fuga dolor rerum dicta, ipsum laboriosam est totam iusto alias incidunt 133 | cum tempore aliquid aliquam error quisquam ipsam asperiores! Iste? 134 |

135 | 136 |
137 |

John Doe - 12th January, 2024

138 |
139 |
140 | 141 |
142 |
143 |
144 | 150 | 153 | 154 | 160 | 163 | 164 | 170 | 173 | 174 | 180 | 183 | 184 | 190 | 193 | 194 |
195 | 196 |

The best thing money can buy!

197 |
198 | 199 |

200 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ullam possimus 201 | fuga dolor rerum dicta, ipsum laboriosam est totam iusto alias incidunt 202 | cum tempore aliquid aliquam error quisquam ipsam asperiores! Iste? 203 |

204 | 205 |
206 |

John Doe - 12th January, 2024

207 |
208 |
209 | 210 |
211 |
212 |
213 | 219 | 222 | 223 | 229 | 232 | 233 | 239 | 242 | 243 | 249 | 252 | 253 | 259 | 262 | 263 |
264 | 265 |

The best thing money can buy!

266 |
267 | 268 |

269 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ullam possimus 270 | fuga dolor rerum dicta, ipsum laboriosam est totam iusto alias incidunt 271 | cum tempore aliquid aliquam error quisquam ipsam asperiores! Iste? 272 |

273 | 274 |
275 |

John Doe - 12th January, 2024

276 |
277 |
278 | 279 |
280 |
281 |
282 | 288 | 291 | 292 | 298 | 301 | 302 | 308 | 311 | 312 | 318 | 321 | 322 | 328 | 331 | 332 |
333 | 334 |

The best thing money can buy!

335 |
336 | 337 |

338 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ullam possimus 339 | fuga dolor rerum dicta, ipsum laboriosam est totam iusto alias incidunt 340 | cum tempore aliquid aliquam error quisquam ipsam asperiores! Iste? 341 |

342 | 343 |
344 |

John Doe - 12th January, 2024

345 |
346 |
347 |
348 |
349 | -------------------------------------------------------------------------------- /src/components/Products.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { z } from "zod"; 3 | import { ProductResult } from "../utils/schemas"; 4 | import ProductCard from "./ProductCard.astro"; 5 | 6 | const ProductsResult = z.array(ProductResult); 7 | export interface Props { 8 | products: z.infer; 9 | } 10 | const { products } = Astro.props as Props; 11 | --- 12 | 13 |
14 |
15 |

Products

16 | 17 |
18 | {products.map((product) => )} 19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /src/components/ShopifyImage.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 | {#if image} 33 | {image.altText image && value < image.width) 43 | .map((value) => { 44 | if (image && image.width >= value) { 45 | return `${imageFilter({ 46 | width: value, 47 | })} ${value}w`; 48 | } 49 | }) 50 | .join(", ") 51 | .concat(`, ${image.url} ${image.width}w`)} 52 | /> 53 | {:else} 54 | 55 | 60 | 63 | 66 | 69 | 72 | 73 | {/if} 74 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/layouts/BaseLayout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Header from "../components/Header.astro"; 3 | import Footer from "../components/Footer.astro"; 4 | import CartDrawer from "../components/CartDrawer.svelte"; 5 | import AnnouncementBar from "../components/AnnouncementBar.astro"; 6 | 7 | export interface Props { 8 | title: string; 9 | description?: string; 10 | } 11 | 12 | const defaultDesc = 13 | "A lightweight and minimalit Astro starter theme using Shopify with Svelte, Tailwind, and TypeScript."; 14 | 15 | const { title, description = defaultDesc } = Astro.props as Props; 16 | --- 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {title} 25 | 26 | 29 | 30 | 31 | 32 | 33 |
34 |
35 | 36 |
37 |