The web framework that scales with you — Build fast content sites, powerful web applications, dynamic server APIs, and everything in-between ️ Star to support our work!
Find a file
Drew Powers 3639190b4e
Renaming to import.meta.fetchContent (#70)
* Change to import.meta.glob()

Change of plans—maintain parity with Snowpack and Vite because our Collections API will use a different interface

* Get basic pagination working

* Get params working

* Rename to import.meta.fetchContent

* Upgrade to fdir
2021-04-12 17:21:29 -06:00
.github Extract Astro styles to external stylesheets (#43) 2021-03-31 13:04:18 -06:00
.vscode Scaffold language server (#25) 2021-03-25 10:38:17 -05:00
astro-prism Add support for syntax highlighting of code blocks (#65) 2021-04-08 15:17:00 -04:00
components Add support for syntax highlighting of code blocks (#65) 2021-04-08 15:17:00 -04:00
examples Renaming to import.meta.fetchContent (#70) 2021-04-12 17:21:29 -06:00
src Renaming to import.meta.fetchContent (#70) 2021-04-12 17:21:29 -06:00
test chore fix lint reduce errors generated (#83) 2021-04-12 16:20:58 +01:00
vscode Scaffold language server (#25) 2021-03-25 10:38:17 -05:00
www Add support for syntax highlighting of code blocks (#65) 2021-04-08 15:17:00 -04:00
.eslintignore Annoying Lint PR #2 (#47) 2021-04-01 10:25:28 -06:00
.eslintrc.cjs Blog Support 1/3: Data fetching (#62) 2021-04-06 15:54:55 -06:00
.gitignore Scaffold language server (#25) 2021-03-25 10:38:17 -05:00
.prettierrc.json Annoying Lint PR™ (#3) 2021-03-16 12:37:45 -06:00
astro.mjs Add a proper cli 2021-03-15 15:26:23 -04:00
LICENSE Bring compiler into Astro (#4) 2021-03-16 16:08:11 -04:00
package-lock.json Renaming to import.meta.fetchContent (#70) 2021-04-12 17:21:29 -06:00
package.json Renaming to import.meta.fetchContent (#70) 2021-04-12 17:21:29 -06:00
README.md Renaming to import.meta.fetchContent (#70) 2021-04-12 17:21:29 -06:00
snowpack-plugin.cjs update snowpack plugin 2021-04-10 22:02:19 -07:00
tsconfig.json Add type declarations (#59) 2021-04-02 21:01:57 -06:00

👩‍🚀 Astro

A next-generation static-site generator with partial hydration. Use your favorite JS framework and ship bare-minimum JS (or none at all!).

🔧 Setup

# currently hidden during private beta, please don't share :)
npm install astro@shhhhh

# NOTE: There is currently a bug in Snowpack that prevents you 
# from using astro outside of the monorepo setup that we have here.
# For now, do all development inside the `examples/` directory for this repo.

🧞 Development

Add a dev npm script to your /package.json file:

{
  "scripts": {
    "dev": "astro dev ."
  }
}

Then run:

npm run dev

⚙️ Configuration

To configure Astro, add a astro.config.mjs file in the root of your project. All of the options can be omitted. Here are the defaults:

export default {
  /** Where to resolve all URLs relative to. Useful if you have a monorepo project. */
  projectRoot: '.',
  /** Path to Astro components, pages, and data */
  astroRoot: './astro',
  /** When running `astro build`, path to final static output */
  dist: './_site',
  /** A folder of static files Astro will copy to the root. Useful for favicons, images, and other files that dont need processing. */
  public: './public',
  /** Extension-specific handlings */
  extensions: {
    /** Set this to "preact" or "react" to determine what *.jsx files should load */
    '.jsx': 'react',
  },
};

💧 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.

  • <MyComponent /> will render an HTML-only version of MyComponent (default)
  • <MyComponent:load /> will render MyComponent on page load
  • <MyComponent:idle /> will use requestIdleCallback() to render MyComponent as soon as main thread is free
  • <MyComponent:visible /> will use an IntersectionObserver to render MyComponent when the element enters the viewport

⚛️ State Management

Frontend state management depends on your framework of choice. Below is a list of popular frontend state management libraries, and their current support with Astro.

Our goal is to support all popular state management libraries, as long as there is no technical reason that we cannot.

  • React/Preact
    • Redux: Partial Support (Note: You can access a Redux store directly, but full react-redux support requires the ability to set a custom <Provider> wrapper to every component island. Planned.)
    • Recoil: Full Support
  • Svelte
    • Svelte Stores: Full Support
  • Vue:
    • Vuex: Partial Support (Note: You can access a vuex store directly, but full vuex support requires the ability to set a custom vue.use(store) call to every component island. Planned.)

Are we missing your favorite state management library? Add it to the list above in a PR (or create an issue)!

💅 Styling

If youve used Sveltes styles before, Astro works almost the same way. In any .astro file, start writing styles in a <style> tag like so:

<style>
  .scoped {
    font-weight: bold;
  }
</style>

<div class="scoped">Im a scoped style</div>

👓 Sass

Astro also supports Sass out-of-the-box; no configuration needed:

<style lang="scss">
  @use "../tokens" as *;

  .title {
    color: $color.gray;
  }
</style>

<h1 class="title">Title</h1>

Supports:

  • lang="scss": load as the .scss extension
  • lang="sass": load as the .sass extension (no brackets; indent-style)

🦊 Autoprefixer

We also automatically add browser prefixes using Autoprefixer. By default, Astro loads the default values, but you may also specify your own by placing a Browserslist file in your project root.

🍃 Tailwind

Astro can be configured to use Tailwind easily! Install the dependencies:

npm install @tailwindcss/jit tailwindcss

And also create a tailwind.config.js in your project root:

module.exports = {
  // your options here
}

Note: a Tailwind config file is currently required to enable Tailwind in Astro, even if you use the default options.

Then write Tailwind in your project just like youre used to:

<style>
  @tailwind base;
  @tailwind components;
  @tailwind utilities;
</style>

🍱 Collections (beta)

Astros Collections API can be used for paginating content whether local *.md files or data from a headless CMS.

First, decide on a URL schema. For our example, perhaps you want all your paginated posts at /posts/1, /posts/2, etc. But in addition, you also wanted /tag/[tag] and /year/[year] collections where posts are filtered by tag or year.

Next, for each “owner” of a URL tree, create a /astro/pages/$[collection].astro file. So in our example, wed need 3:

└── astro/
    └── pages/
        ├── $posts.astro     -> /posts/1, /posts/2, …
        ├── $tag.astro       -> /tag/[tag]/1, /tag/[tag]/2, …
        └── $year.astro      -> /year/[year]/1, /year/[year]/2, …

Lastly, in each $[collection].astro file, add 2 things:

export let collection: any;
export async function createCollection() {
  return {
    async data() {
      // return data here to load (well 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

Assume we have Markdown files that have title, tag, and date in their frontmatter, like so:

---
title: My Blog Post
tag: javascript
date: 2021-03-01 09:34:00
---

# My Blog post

Its important to know that these could be anything! Theres no restrictions around what can go in your frontmatter, but these will explain values we see later. Assume nothing is “special“ or reserved; we named everything.

Also, assume we want the following final routes:

  • Individual blog posts live at /post/[slug].
  • The paginated blog posts live at /posts/1 for page 1, /posts/2 for page 2, etc.
  • We also want to add /tag/[tag]/1 for tagged posts, page 1, or /year/[year]/1 for posts by year. Well add these at the end.

Lets start with paginated posts. Since we want /posts/ to be the root, well create a file at /astro/pages/$posts.astro (the $ indicates that this is a multi-route page):

// /astro/pages/$posts.astro
---
import Pagination from '../components/Pagination.astro';
import PostPreview from '../components/PostPreview.astro';

export let collection: any;

export async function createCollection() {
  const allPosts = import.meta.fetchContent('./post/*.md');     // load data that already lives at `/post/[slug]`
  allPosts.sort((a, b) => new Date(b.date) - new Date(a.date)); // sort newest -> oldest (we got "date" 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)
  };
}
---
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Blog Posts: page {collection.page.current}</title>
    <link rel="canonical" href={collection.url.current} />
    <link rel="prev" href={collection.url.prev} />
    <link rel="next" href={collection.url.next} />
  </head>
  <body>
    <main>
      <h5>Results {collection.start + 1}{collection.start + 1 + collection.page.size} of {collection.total}</h6>
      {collection.data.map((post) => (
        <PostPreview post={post} />
      )}
    </main>
    <footer>
      <Pagination
        currentPage={collection.page.current}
        totalPages={collection.page.last}
        prevURL={collection.url.prev}
        nextURL={collection.url.next}
      />
    </footer>
  </body>
</html>

Lets 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 well 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.

It should be noted that the above example shows <PostPreview /> and <Pagination /> components. Pretend those are custom components that you made to display the post data, and the pagination navigation. Theres nothing special about them; only consider those examples of how youd use collection data to display everything the way youd like.

Example 2: Advanced filtering & pagination

In our earlier example, we covered simple pagination for /posts/1, but wed still like to make /tag/[tag]/1 and /year/[year]/1. To do that, well create 2 more collections: /astro/pages/$tag.astro and astro/pages/$year.astro. Assume that the markup is the same, but weve expanded the createCollection() function with more data.

  // /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 = import.meta.fetchContent('./post/*.md');
    allPosts.sort((a, b) => new Date(b.date) - new Date(a.date));
+   const allTags = [...new Set(allPosts.map((post) => post.tags).flat())];  // gather all unique tags (we got "tag" 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.tag === params.tag);           // filter post by "date" frontmatter, from params (we get `{ params }` from the routes array above)
      },
      pageSize: 10,
+     routes,
+     permalink: ({ params }) => `/tag/${params.tag}/`                       // important! the root must match (/tag/[tag] -> $tag.astro)
    };
  }
  ---

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 isnt 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 (its best **NOT** to use /pages/$tag.astroto generate/year/$year.astro; that should live at /pages/$year.astro` as a separate file).
  • allPosts.filter((post) => post.tag === params.tag): we arent returning all posts here; were 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.

Lastly, what about /year/*? Well hopefully you can figure that out from here. It follows the exact same pattern, except using post.date frontmatter. Youll grab the year from that date string, and sort probably newest to oldest rather than alphabetical. Youll also change params.tag to params.year (or whatever you name it), but otherwise most everything else should be the same.

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. Dont try and trick permalink to generate too many URL trees; itll only result in pages being missed when it comes time to build.
  • Need to load local markdown? Try import.meta.fetchContent('./data/*.md')
  • Need to load remote data? Simply fetch() to make it happen!

🚀 Build & Deployment

Add a build npm script to your /package.json file:

{
  "scripts": {
    "dev": "astro dev .",
    "build": "astro build ."
  }
}

Then run:

npm run build

Now upload the contents of /_site_ to your favorite static site host.