From 76932822b8fff54e56812b7fc90450904ad276ae Mon Sep 17 00:00:00 2001 From: Drew Powers <1369770+drwpow@users.noreply.github.com> Date: Tue, 20 Apr 2021 17:15:47 -0600 Subject: [PATCH] Split README into docs (#118) --- README.md | 388 ++++++++------------------------------------ docs/api.md | 97 +++++++++++ docs/collections.md | 195 ++++++++++++++++++++++ docs/styling.md | 126 ++++++++++++++ 4 files changed, 487 insertions(+), 319 deletions(-) create mode 100644 docs/api.md create mode 100644 docs/collections.md create mode 100644 docs/styling.md diff --git a/README.md b/README.md index 415060afa6..73bd618e1a 100644 --- a/README.md +++ b/README.md @@ -55,14 +55,53 @@ export default { }; ``` +## 🥾 Guides + +### 🚀 Basic Usage + +Even though nearly-everything [is configurable][config], we recommend starting out by creating an `astro/` folder in your project with the following structure: + +``` +├── astro/ +│ ├── components/ +│ └── pages/ +│ └── index.astro +├── public/ +└── package.json +``` + +- `astro/components/*`: where your reusable components go. You can place these anywhere, but we recommend a single folder to keep them organized. +- `astro/pages/*`: this is a special folder where your [routing][routing] lives. + +#### 🚦 Routing + +Routing happens in `astro/pages/*`. Every `.astro` or `.md.astro` file in this folder corresponds with a public URL. For example: + +| Local file | Public URL | +| :--------------------------------------- | :------------------------------ | +| `astro/pages/index.astro` | `/index.html` | +| `astro/pages/post/my-blog-post.md.astro` | `/post/my-blog-post/index.html` | + +#### 🗂 Static Assets + +Static assets should be placed in a `public/` folder in your project. You can place any images, fonts, files, or global CSS in here you need to reference. + +#### 🪨 Generating HTML with Astro + +TODO: Astro syntax guide + +#### ⚡ Dynamic Components + +TODO: Astro dynamic components guide + ### 💧 Partial Hydration By default, Astro outputs zero client-side JS. If you'd like to include an interactive component in the client output, you may use any of the following techniques. - `` will render an HTML-only version of `MyComponent` (default) - `` will render `MyComponent` on page load -- `` will use [requestIdleCallback()][request-idle-cb] to render `MyComponent` as soon as main thread is free -- `` will use an [IntersectionObserver][intersection-observer] to render `MyComponent` when the element enters the viewport +- `` will use [requestIdleCallback()][mdn-ric] to render `MyComponent` as soon as main thread is free +- `` will use an [IntersectionObserver][mdn-io] to render `MyComponent` when the element enters the viewport ### ⚛️ State Management @@ -93,282 +132,40 @@ Styling in Astro is meant to be as flexible as you’d like it to be! The follow ¹ _`.astro` files have no runtime, therefore Scoped CSS takes the place of CSS Modules (styles are still scoped to components, but don’t need dynamic values)_ -#### 🖍 Styling by Framework +To learn more about writing styles in Astro, see our [Styling Guide][docs-styling]. -##### Astro +👉 [**Styling**][docs-styling] -Styling in an Astro component is done by adding a ` - -
I’m a scoped style and only apply to this component
-

I have both scoped and global styles

-``` - -**Tips** - -- ` -``` - -You should see Tailwind styles compile successfully in Astro. - -💁 **Tip**: to reduce duplication, try loading `@tailwind base` from a parent page (`./pages/*.astro`) instead of the component itself. - -## 🚀 Build & Deployment +### 🚀 Build & Deployment Add a `build` npm script to your `/package.json` file: @@ -391,62 +188,15 @@ Now upload the contents of `/_site_` to your favorite static site host. ## 📚 API -### `Astro` global +👉 [**Full API Reference**][docs-api] -The `Astro` global is available in all contexts in `.astro` files. It has the following functions: - -#### `config` - -`Astro.config` returns an object with the following properties: - -| Name | Type | Description | -| :----- | :------- | :--------------------------------------------------------------------------------------------------------- | -| `site` | `string` | Your website’s public root domain. Set it with `site: "https://mysite.com"` in your [Astro config][config] | - -#### `fetchContent()` - -`Astro.fetchContent()` is a way to load local `*.md` files into your static site setup. You can either use this on its own, or within [Astro Collections][collections]. - -``` -// ./astro/components/my-component.astro ---- -const data = Astro.fetchContent('../pages/post/*.md'); // returns an array of posts that live at ./astro/pages/post/*.md ---- - -
-{data.slice(0, 3).map((post) => ( -
-

{post.title}

-

{post.description}

- Read more -
-))} -
-``` - -`.fetchContent()` only takes one parameter: a relative URL glob of which local files you’d like to import. Currently only `*.md` files are supported. It’s synchronous, and returns an array of items of type: - -``` -{ - url: string; // the URL of this item (if it’s in pages/) - content: string; // the HTML of this item - // frontmatter data expanded here -}[]; -``` - -[autoprefixer]: https://github.com/postcss/autoprefixer -[browserslist]: https://github.com/browserslist/browserslist -[collections]: #-collections-beta -[css-modules]: https://github.com/css-modules/css-modules [config]: #%EF%B8%8F-configuration -[fetch-content]: #fetchContent-- -[intersection-observer]: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API -[request-idle-cb]: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback -[sass]: https://sass-lang.com/ -[sass-use]: https://sass-lang.com/documentation/at-rules/use -[svelte]: https://svelte.dev -[svelte-style]: https://svelte.dev/docs#style -[tailwind]: https://tailwindcss.com -[tailwind-utilities]: https://tailwindcss.com/docs/adding-new-utilities#using-css -[vue-css-modules]: https://vue-loader.vuejs.org/guide/css-modules.html -[vue-scoped]: https://vue-loader.vuejs.org/guide/scoped-css.html +[docs-api]: ./docs/api.md +[docs-collections]: ./docs/collections.md +[docs-styling]: ./docs/styling.md +[example-blog]: ./examples/blog +[fetch-content]: ./docs/api.md#fetchcontent +[fetch-js]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API +[mdn-io]: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API +[mdn-ric]: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback +[routing]: #-routing diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000000..b1c106ef88 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,97 @@ +## 📚 API + +### `Astro` global + +The `Astro` global is available in all contexts in `.astro` files. It has the following functions: + +#### `config` + +`Astro.config` returns an object with the following properties: + +| Name | Type | Description | +| :----- | :------- | :--------------------------------------------------------------------------------------------------------- | +| `site` | `string` | Your website’s public root domain. Set it with `site: "https://mysite.com"` in your [Astro config][config] | + +#### `fetchContent()` + +`Astro.fetchContent()` is a way to load local `*.md` files into your static site setup. You can either use this on its own, or within [Astro Collections][docs-collections]. + +```jsx +// ./astro/components/my-component.astro +--- +const data = Astro.fetchContent('../pages/post/*.md'); // returns an array of posts that live at ./astro/pages/post/*.md +--- + +
+{data.slice(0, 3).map((post) => ( +
+

{post.title}

+

{post.description}

+ Read more +
+))} +
+``` + +`.fetchContent()` only takes one parameter: a relative URL glob of which local files you’d like to import. Currently only `*.md` files are supported. It’s synchronous, and returns an array of items of type: + +``` +{ + url: string; // the URL of this item (if it’s in pages/) + content: string; // the HTML of this item + // frontmatter data expanded here +}[]; +``` + +### `collection` + +```jsx +export let collection; +``` + +When using the [Collections API][docs-collections], `collection` is a prop exposed to the page with the following shape: + +| Name | Type | Description | +| :------------------------ | :-------------------: | :-------------------------------------------------------------------------------------------------------------------------------- | +| `collection.data` | `Array` | Array of data returned from `data()` for the current page. | +| `collection.start` | `number` | Index of first item on current page, starting at `0` (e.g. if `pageSize: 25`, this would be `0` on page 1, `25` on page 2, etc.). | +| `collection.end` | `number` | Index of last item on current page. | +| `collection.total` | `number` | The total number of items across all pages. | +| `collection.page.current` | `number` | The current page number, starting with `1`. | +| `collection.page.size` | `number` | How many items per-page. | +| `collection.page.last` | `number` | The total number of pages. | +| `collection.url.current` | `string` | Get the URL of the current page (useful for canonical URLs) | +| `collection.url.prev` | `string \| undefined` | Get the URL of the previous page (will be `undefined` if on page 1). | +| `collection.url.next` | `string \| undefined` | Get the URL of the next page (will be `undefined` if no more pages). | +| `collection.params` | `object` | If page params were used, this returns a `{ key: value }` object of all values. | + +### `createCollection()` + +```jsx +export async function createCollection() { + return { + async data({ params }) { + // load data + }, + pageSize: 25, + routes: [{ tag: 'movie' }, { tag: 'television' }], + permalink: ({ params }) => `/tag/${params.tag}`, + }; +} +``` + +When using the [Collections API][docs-collections], `createCollection()` is an async function that returns an object of the following shape: + +| Name | Type | Description | +| :---------- | :---------------------------: | :--------------------------------------------------------------------------------------------------------- | +| `data` | `async ({ params }) => any[]` | **Required.** Load data with this function to be returned. | +| `pageSize` | `number` | Specify number of items per page (default: `25`). | +| `routes` | `params[]` | **Required for URL Params.** Return an array of all possible URL `param` values in `{ name: value }` form. | +| `permalink` | `({ params }) => string` | **Required for URL Params.** Given a `param` object of `{ name: value }`, generate the final URL.\* | + +_\* Note: don’t create confusing URLs with `permalink`, e.g. rearranging params conditionally based on their values._ + +⚠️ `createCollection()` executes in its own isolated scope before page loads. Therefore you can’t reference anything from its parent scope. If you need to load data you may fetch or use async `import()`s within the function body for anything you need (that’s why it’s `async`—to give you this ability). If it wasn’t isolated, then `collection` would be undefined! Therefore, duplicating imports between `createCollection()` and your Astro component is OK. + +[config]: ../README.md#%EF%B8%8F-configuration +[docs-collections]: ./collections.md diff --git a/docs/collections.md b/docs/collections.md new file mode 100644 index 0000000000..f6599e3dc8 --- /dev/null +++ b/docs/collections.md @@ -0,0 +1,195 @@ +# 🍱 Collections + +## ❓ What are Collections? + +[Fetching data is easy in Astro][docs-data]. But what if you wanted to make a paginated blog? What if you wanted an easy way to sort data, or filter, say, by a given tag? When you need something a little more powerful than simple data fetching, Astro’s Collections API may be what you need. + +An Astro Collection is similar to the general concept of Collections in static site generators like Jekyll, Hugo, Eleventy, etc. It’s a general way to load an entire data set. But one big difference between Astro Collections and traditional static site generators is: **Astro lets you seamlessly blend remote API data and local files in a JAMstack-friendly way.** To see how, this guide will walk through a few examples. If you’d like, you can reference the [blog example project][example-blog] to see the finished code in context. + +## 🧑‍🎨 How to Use + +By default, any Astro component can fetch data from any API or local `*.md` files. But what if you had a blog you wanted to paginate? What if you wanted to generate dynamic URLs based on metadata (e.g. `/tag/:tag/`)? Or do both together? Astro Collections are a way to do all of that. It’s perfect for generating blog-like content, or scaffolding out dynamic URLs from your data. + +Let’s pretend we have some blog posts written already. This is our starting project structure: + +``` +└── astro/ + └── pages/ + └── post/ + └── (blog content) +``` + +The first step in adding some dynamic collections is deciding on a URL schema. For our example website, we’re aiming for the following URLs: + +- `/post/:post`: A single blog post page +- `/posts/:page`: A list page of all blog posts, paginated, and sorted most recent first +- `/tag/:tag`: All blog posts, filtered by a specific tag + +Because `/post/:post` references the static files we have already, that doesn’t need to be a collection. But we will need collections for `/posts/:page` and `/tag/:tag` because those will be dynamically generated. For both collections we’ll create a `/astro/pages/$[collection].astro` file. This is our new structure: + +```diff + └── astro/ + └── pages/ + ├── post/ + │ └── (blog content) ++ ├── $posts.astro -> /posts/1, /posts/2, … ++ └── $tag.astro -> /tag/:tag/1, /tag/:tag/2, … +``` + +💁‍ **Tip**: Any `.astro` filename beginning with a `$` is how it’s marked as a collection. + +In each `$[collection].astro` file, we’ll need 2 things: + +```js +// 1. We need to mark “collection” as a prop (this is a special reserved name) +export let collection: any; + +// 2. We need to export an async createCollection() function that will retrieve our data. +export async function createCollection() { + return { + async data() { + // return data here to load (we’ll cover how later) + }, + }; +} +``` + +These are important so your data is exposed to the page as a prop, and also Astro has everything it needs to gather your data and generate the proper routes. How it does this is more clear if we walk through a practical example. + +#### Example 1: Simple pagination + +Our blog posts all contain `title`, `tags`, and `published_at` in their frontmatter: + +```md +--- +title: My Blog Post +tags: + - javascript +published_at: 2021-03-01 09:34:00 +--- + +# My Blog post + +… +``` + +There’s nothing special or reserved about any of these names; you’re free to name everything whatever you’d like, or have as much or little frontmatter as you need. + +```jsx +// /astro/pages/$posts.astro +--- +export let collection: any; + +export async function createCollection() { + const allPosts = Astro.fetchContent('./post/*.md'); // load data that already lives at `/post/:slug` + allPosts.sort((a, b) => new Date(b.published_at) - new Date(a.published_at)); // sort newest -> oldest (we got "published_at" from frontmatter!) + + // (load more data here, if needed) + + return { + async data() { + return allPosts; + }, + pageSize: 10, // how many we want to show per-page (default: 25) + }; +} + +function formatDate(date) { + return new Date(date).toUTCString(); +} +--- + + + + Blog Posts: page {collection.page.current} + + + + + +
+
Results {collection.start + 1}–{collection.end + 1} of {collection.total}
+ {collection.data.map((post) => ( +

{post.title}

+ + Read + )} +
+
+

Page {collection.page.current} / {collection.page.last}

+ +
+ + +``` + +Let’s walk through some of the key parts: + +- `export let collection`: this is important because it exposes a prop to the page for Astro to return with all your data loaded. ⚠️ **It must be named `collection`**. +- `export async function createCollection()`: this is also required, **and must be named this exactly.** This is an async function that lets you load data from anywhere (even a remote API!). At the end, you must return an object with `{ data: yourData }`. There are other options such as `pageSize` we’ll cover later. +- `{collection.data.map((post) => (…`: this lets us iterate over all the markdown posts. This will take the shape of whatever you loaded in `createCollection()`. It will always be an array. +- `{collection.page.current}`: this, and other properties, simply return more info such as what page a user is on, what the URL is, etc. etc. +- Curious about everything on `collection`? See the [reference][collection-api]. + +#### Example 2: Advanced filtering & pagination + +In our earlier example, we covered simple pagination for `/posts/1`, but we’d still like to make `/tag/:tag/1` and `/year/:year/1`. To do that, we’ll create 2 more collections: `/astro/pages/$tag.astro` and `astro/pages/$year.astro`. Assume that the markup is the same, but we’ve expanded the `createCollection()` function with more data. + +```diff + // /astro/pages/$tag.astro + --- + import Pagination from '../components/Pagination.astro'; + import PostPreview from '../components/PostPreview.astro'; + + export let collection: any; + + export async function createCollection() { + const allPosts = Astro.fetchContent('./post/*.md'); + allPosts.sort((a, b) => new Date(b.published_at) - new Date(a.published_at)); ++ const allTags = [...new Set(allPosts.map((post) => post.tags).flat())]; // gather all unique tags (we got "tags" from frontmatter!) ++ allTags.sort((a, b) => a.localeCompare(b)); // sort tags A -> Z ++ const routes = allTags.map((tag) => ({ tag })); // this is where we set { params: { tag } } + + return { +- async data() { +- return allPosts; ++ async data({ params }) { ++ return allPosts.filter((post) => post.tags.includes(params.tag)); // filter posts that match the :tag from the URL ("params") + }, + pageSize: 10, ++ routes, ++ permalink: ({ params }) => `/tag/${params.tag}/` // this is where we generate our URL structure + }; + } + --- +``` + +Some important concepts here: + +- `routes = allTags.map((tag) => ({ tag }))`: Astro handles pagination for you automatically. But when it needs to generate multiple routes, this is where you tell Astro about all the possible routes. This way, when you run `astro build`, your static build isn’t missing any pages. +- `permalink: ({ params }) => `/tag/${params.tag}/`: this is where you tell Astro what the generated URL should be. Note that while you have control over this, the root of this must match the filename (it’s best **NOT** to use `/pages/$tag.astro`to generate`/year/$year.astro`; that should live at `/pages/$year.astro` as a separate file). +- `allPosts.filter((post) => post.tag === params.tag)`: we aren’t returning all posts here; we’re only returning posts with a matching tag. _What tag,_ you ask? The `routes` array has `[{ tag: 'javascript' }, { tag: '…`, and all the routes we need to gather. So we first need to query everything, but only return the `.filter()`ed posts at the very end. + +Other things of note is that we are sorting like before, but we filter by the frontmatter `tag` property, and return those at URLs. + +These are still paginated, too! But since there are other conditions applied, they live at a different URL. + +#### Tips + +- Having to load different collections in different `$[collection].astro` files might seem like a pain at first, until you remember **you can create reusable components!** Treat `/pages/*.astro` files as your one-off routing & data fetching logic, and treat `/components/*.astro` as your reusable markup. If you find yourself duplicating things too much, you can probably use a component instead! +- Stay true to `/pages/$[collection].astro` naming. If you have an `/all-posts/*` route, then use `/pages/$all-posts.astro` to manage that. Don’t try and trick `permalink` to generate too many URL trees; it’ll only result in pages being missed when it comes time to build. + +### 📚 Further Reading + +- [Fetching data in Astro][docs-data] +- API Reference: [collection][collection-api] +- API Reference: [createCollection()][create-collection-api] + +[docs-data]: ../README.md#-fetching-data +[collection-api]: ./api.md#collection +[create-collection-api]: ./api.md#createcollection +[example-blog]: ../examples/blog +[fetch-content]: ./api.md#fetchcontent diff --git a/docs/styling.md b/docs/styling.md new file mode 100644 index 0000000000..e5546e887e --- /dev/null +++ b/docs/styling.md @@ -0,0 +1,126 @@ +# 💅 Styling + +Styling in Astro is meant to be as flexible as you’d like it to be! The following options are all supported: + +| Framework | Global CSS | Scoped CSS | CSS Modules | +| :--------------- | :--------: | :--------: | :---------: | +| Astro (`.astro`) | ✅ | ✅ | N/A¹ | +| React / Preact | ✅ | ❌ | ✅ | +| Vue | ✅ | ✅ | ✅ | +| Svelte | ✅ | ✅ | ❌ | + +¹ _`.astro` files have no runtime, therefore Scoped CSS takes the place of CSS Modules (styles are still scoped to components, but don’t need dynamic values)_ + +#### 🖍 Styling by Framework + +##### Astro + +Styling in an Astro component is done by adding a ` + +
I’m a scoped style and only apply to this component
+

I have both scoped and global styles

+``` + +**Tips** + +- ` +``` + +You should see Tailwind styles compile successfully in Astro. + +💁 **Tip**: to reduce duplication, try loading `@tailwind base` from a parent page (`./pages/*.astro`) instead of the component itself. + +[autoprefixer]: https://github.com/postcss/autoprefixer +[browserslist]: https://github.com/browserslist/browserslist +[css-modules]: https://github.com/css-modules/css-modules +[vue-css-modules]: https://vue-loader.vuejs.org/guide/css-modules.html +[vue-scoped]: https://vue-loader.vuejs.org/guide/scoped-css.html +[sass]: https://sass-lang.com/ +[sass-use]: https://sass-lang.com/documentation/at-rules/use +[svelte-style]: https://svelte.dev/docs#style +[tailwind]: https://tailwindcss.com +[tailwind-utilities]: https://tailwindcss.com/docs/adding-new-utilities#using-css