├── .changeset ├── README.md └── config.json ├── .github └── workflows │ ├── deploy-docs.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── docs ├── .gitignore ├── README.md ├── astro.config.mjs ├── package.json ├── public │ └── favicon.svg ├── src │ ├── components │ │ └── CollectionSchema.astro │ ├── content │ │ ├── config.ts │ │ └── docs │ │ │ ├── index.mdx │ │ │ └── reference │ │ │ ├── collections.mdx │ │ │ └── utilities.mdx │ ├── env.d.ts │ ├── overrides │ │ └── Head.astro │ └── pages │ │ └── og │ │ ├── [...slug].ts │ │ └── _logo.png └── tsconfig.json ├── examples └── blog-starter │ ├── .gitignore │ ├── .vscode │ ├── extensions.json │ └── launch.json │ ├── README.md │ ├── astro.config.mjs │ ├── package.json │ ├── public │ ├── favicon.svg │ └── fonts │ │ ├── atkinson-bold.woff │ │ └── atkinson-regular.woff │ ├── src │ ├── components │ │ ├── BaseHead.astro │ │ ├── Comments.astro │ │ ├── Footer.astro │ │ ├── FormattedDate.astro │ │ ├── Header.astro │ │ ├── HeaderLink.astro │ │ ├── PageIndex.astro │ │ └── PageTree.astro │ ├── content │ │ └── config.ts │ ├── layouts │ │ ├── BaseLayout.astro │ │ ├── BlogPost.astro │ │ ├── Category.astro │ │ ├── Page.astro │ │ └── Tag.astro │ ├── pages │ │ ├── [...slug].astro │ │ ├── blog │ │ │ ├── [...slug].astro │ │ │ └── index.astro │ │ ├── category │ │ │ └── [slug].astro │ │ ├── index.astro │ │ ├── rss.xml.ts │ │ └── tag │ │ │ └── [slug].astro │ └── styles │ │ └── global.css │ └── tsconfig.json ├── package.json ├── packages └── dewp │ ├── CHANGELOG.md │ ├── README.md │ ├── content-utils.ts │ ├── loaders.ts │ ├── package.json │ └── schemas.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── tsconfig.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.3/schema.json", 3 | "changelog": ["@changesets/changelog-github", { "repo": "delucis/dewp" }], 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": ["dewp-docs", "@examples/*"] 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | # Trigger the workflow every time you push to the `main` branch 5 | # Using a different branch name? Replace `main` with your branch’s name 6 | push: 7 | branches: [main] 8 | # Allows you to run this workflow manually from the Actions tab on GitHub. 9 | workflow_dispatch: 10 | 11 | # Allow this job to clone the repo and create a page deployment 12 | permissions: 13 | contents: read 14 | pages: write 15 | id-token: write 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout your repository using git 22 | uses: actions/checkout@v4 23 | - name: Install, build, and upload your site output 24 | uses: withastro/action@v3 25 | with: 26 | path: ./docs 27 | node-version: 20 28 | package-manager: pnpm 29 | 30 | deploy: 31 | needs: build 32 | runs-on: ubuntu-latest 33 | environment: 34 | name: github-pages 35 | url: ${{ steps.deployment.outputs.page_url }} 36 | steps: 37 | - name: Deploy to GitHub Pages 38 | id: deployment 39 | uses: actions/deploy-pages@v4 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | if: ${{ github.repository_owner == 'delucis' }} 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout Repo 15 | uses: actions/checkout@v4 16 | with: 17 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 18 | fetch-depth: 0 19 | 20 | - name: Setup PNPM 21 | uses: pnpm/action-setup@v3 22 | 23 | - name: Setup Node 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 20 27 | cache: 'pnpm' 28 | 29 | - name: Install Dependencies 30 | run: pnpm i 31 | 32 | - name: Create Release Pull Request 33 | uses: changesets/action@v1 34 | with: 35 | version: pnpm run version 36 | publish: pnpm changeset publish 37 | commit: '[ci] release' 38 | title: '[ci] release' 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules/ 3 | 4 | # logs 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | pnpm-debug.log* 9 | 10 | # environment variables 11 | .env 12 | .env.production 13 | 14 | # macOS-specific files 15 | .DS_Store 16 | 17 | # build output 18 | dist/ 19 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | prefer-workspace-packages=true 2 | link-workspace-packages=true 3 | 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Chris Swithinbank 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 | packages/dewp/README.md -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 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 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # DeWP Docs 2 | 3 | This directory contains a [Starlight](https://starlight.astro.build) site for the DeWP documentation 4 | 5 | ## 🧞 Commands 6 | 7 | All commands are run from the root of the project, from a terminal: 8 | 9 | | Command | Action | 10 | | :------------------------- | :----------------------------------------------- | 11 | | `pnpm install` | Installs dependencies | 12 | | `pnpm run dev` | Starts local dev server at `localhost:4321` | 13 | | `pnpm run build` | Build your production site to `./dist/` | 14 | | `pnpm run preview` | Preview your build locally, before deploying | 15 | | `pnpm run astro ...` | Run CLI commands like `astro add`, `astro check` | 16 | | `pnpm run astro -- --help` | Get help using the Astro CLI | 17 | -------------------------------------------------------------------------------- /docs/astro.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { defineConfig } from 'astro/config'; 3 | import starlight from '@astrojs/starlight'; 4 | 5 | // https://astro.build/config 6 | export default defineConfig({ 7 | site: 'https://delucis.github.io', 8 | base: '/dewp', 9 | integrations: [ 10 | starlight({ 11 | title: 'DeWP', 12 | description: 'Use your WordPress data in Astro projects', 13 | social: { 14 | github: 'https://github.com/delucis/dewp', 15 | }, 16 | sidebar: ['index', { label: 'Reference', autogenerate: { directory: 'reference' } }], 17 | components: { 18 | Head: './src/overrides/Head.astro', 19 | }, 20 | }), 21 | ], 22 | }); 23 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dewp-docs", 3 | "type": "module", 4 | "private": true, 5 | "version": "0.0.1", 6 | "scripts": { 7 | "dev": "astro dev", 8 | "start": "astro dev", 9 | "build": "astro check && astro build", 10 | "preview": "astro preview", 11 | "astro": "astro" 12 | }, 13 | "dependencies": { 14 | "@astrojs/check": "^0.9.4", 15 | "@astrojs/starlight": "^0.28.3", 16 | "astro": "^4.16.18", 17 | "astro-og-canvas": "^0.5.4", 18 | "canvaskit-wasm": "^0.39.1", 19 | "sharp": "^0.32.5", 20 | "starlight-package-managers": "^0.7.0", 21 | "typescript": "^5.6.3", 22 | "zod-to-json-schema": "^3.23.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docs/public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/src/components/CollectionSchema.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { zodToJsonSchema } from 'zod-to-json-schema'; 3 | import * as schemas from '../../../packages/dewp/schemas'; 4 | 5 | interface Props { 6 | collection: keyof typeof schemas; 7 | } 8 | 9 | const zodSchema = schemas[Astro.props.collection]; 10 | const { definitions } = zodToJsonSchema(zodSchema, 'schema'); 11 | 12 | if (!definitions?.schema) { 13 | throw new Error(); 14 | } 15 | const { schema } = definitions; 16 | 17 | type JsonSchema7Type = (typeof definitions)[string]; 18 | 19 | let entries: [key: string, schema: JsonSchema7Type][] = []; 20 | if ('type' in schema && schema.type === 'object' && 'properties' in schema) { 21 | entries = Object.entries(schema.properties || {}); 22 | } 23 | 24 | const getDefault = (schema: JsonSchema7Type) => { 25 | return 'default' in schema ? JSON.stringify(schema.default) : ''; 26 | }; 27 | 28 | const getTypeString = (schema: JsonSchema7Type): string => { 29 | if (!('type' in schema)) { 30 | return ''; 31 | } 32 | if ('enum' in schema) { 33 | return schema.enum.map((i) => JSON.stringify(i)).join(' | '); 34 | } 35 | if (schema.type === 'object') { 36 | if ('properties' in schema) { 37 | return ( 38 | '{ ' + 39 | Object.entries(schema.properties) 40 | .map(([key, schema]): string => `${key}: ${getTypeString(schema)}`) 41 | .join('; ') + 42 | ' }' 43 | ); 44 | } else if ('additionalProperties' in schema) { 45 | return `Record`; 46 | } 47 | } 48 | return Array.isArray(schema.type) ? schema.type.join(' | ') : schema.type; 49 | }; 50 | --- 51 | 52 |

Entry data

53 | { 54 | entries 55 | .sort(([a], [b]) => (a > b ? 1 : -1)) 56 | .map(([key, schema]) => { 57 | const type = getTypeString(schema); 58 | const dflt = getDefault(schema); 59 | return ( 60 | <> 61 |
62 | {key} 63 |
64 | {(type || dflt) && ( 65 |

66 | {type && ( 67 | <> 68 | Type: {type} 69 | 70 | )} 71 | {dflt && type &&
} 72 | {dflt && ( 73 | <> 74 | Default: {dflt} 75 | 76 | )} 77 |

78 | )} 79 |

{schema.description}

80 | 81 | ); 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /docs/src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection } from 'astro:content'; 2 | import { docsSchema } from '@astrojs/starlight/schema'; 3 | 4 | export const collections = { 5 | docs: defineCollection({ schema: docsSchema() }), 6 | }; 7 | -------------------------------------------------------------------------------- /docs/src/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting started 3 | description: Use your WordPress data in Astro projects 4 | tableOfContents: false 5 | hero: 6 | title: DeWP 7 | tagline: Use your WordPress data in Astro projects 8 | --- 9 | 10 | import { Card, CardGrid, Steps } from '@astrojs/starlight/components'; 11 | import { PackageManagers } from 'starlight-package-managers'; 12 | 13 | :::note 14 | 15 | This is early-release software. 16 | Please [report bugs](https://github.com/delucis/dewp) that you find. 17 | 18 | `dewp` requires Astro v5, which is currently in beta. 19 | 20 | ::: 21 | 22 | ## Features 23 | 24 | This project contains several tools for using your WordPress data in Astro projects. 25 | You can use them to build headless WordPress sites with Astro or even migrate your data away from WordPress to Astro entirely. 26 | 27 | 28 | 29 | `dewp/loaders` loads your content using the WordPress REST API so you can use it with Astro’s 30 | content collection API. 31 | 32 | 33 | Use DeWP’s blog template to get started with an Astro project using headless WordPress data. 34 | 35 | 36 | 37 | More coming soon! 38 | 39 | ## Create a new project 40 | 41 | The easiest way to get started is to create a new Astro project using the DeWP blog template. 42 | 43 | 44 | 45 | 1. Run the following command in your terminal and follow the instructions in the Astro install wizard. 46 | 47 | 52 | 53 | :::tip[Browser preview] 54 | You can also test a [preview of the template on StackBlitz](https://stackblitz.com/github/delucis/dewp/tree/main/examples/blog-starter). 55 | ::: 56 | 57 | 2. Update the `endpoint` option in `src/content/config.ts` to the URL of your WordPress REST API. Most commonly this is the URL of your website with `/wp-json/` at the end. 58 | 59 | ```ts title="src/content/config.ts" "'https://example.com/wp-json/'" 60 | import { wpCollections } from 'dewp/loaders'; 61 | 62 | export const collections = wpCollections({ 63 | endpoint: 'https://example.com/wp-json/', 64 | }); 65 | ``` 66 | 67 | 3. You can now [start the Astro dev server](https://docs.astro.build/en/install-and-setup/#start-the-astro-dev-server) and see a live preview of your project while you build. 68 | 69 | 70 | 71 | The template shows links to all the pages in the linked WordPress site on the homepage and links to the 10 most recent blog posts at `/blog`. 72 | 73 | ## Add to an existing project 74 | 75 | If you prefer to add the DeWP content loaders to an existing Astro project, follow these steps. 76 | 77 | 78 | 79 | 1. Install DeWP by running the following command in your project directory. 80 | 81 | 82 | 83 | 2. Add the DeWP content loaders to `src/content/config.ts`, setting `endpoint` to the URL of your WordPress REST API. 84 | 85 | ```ts title="src/content/config.ts" ins={2,6} 86 | import { z, defineCollection } from 'astro:content'; 87 | import { wpCollections } from 'dewp/loaders'; 88 | 89 | export const collections = { 90 | // existing collections ... 91 | ...wpCollections({ endpoint: 'https://example.com/wp-json/' }), 92 | }; 93 | ``` 94 | 95 | 3. You can now use Astro’s [content collection APIs](https://5-0-0-beta.docs.astro.build/en/guides/content-collections/) to get your WordPress data in your components. 96 | 97 | In the following example, the `posts` collection is loaded in order to display each post’s title: 98 | 99 | ```astro title="example.astro" 100 | --- 101 | import { getCollection } from 'astro:content'; 102 | const posts = await getCollection('posts'); 103 | --- 104 | {posts.map((post) =>

{post.data.title}

)} 105 | ``` 106 | 107 |
108 | -------------------------------------------------------------------------------- /docs/src/content/docs/reference/collections.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Collections Reference 3 | description: In-depth reference documentation for all the DeWP content collections and their data schemas. 4 | --- 5 | 6 | import CollectionSchema from '../../../components/CollectionSchema.astro'; 7 | 8 | This reference page documents in detail the collections added to an Astro project by DeWP’s collection loaders. 9 | 10 | ## Data structure 11 | 12 | Each collection corresponds to one of WordPress’s data types available from the [WordPress REST API](https://developer.wordpress.org/rest-api/) and in general matches the schema of those endpoints. 13 | 14 | One important difference between the JSON responses returned by the REST API and DeWP’s content collections is that references between collections are encoded as [Astro collection references](https://5-0-0-beta.docs.astro.build/en/guides/content-collections/#defining-collection-references) instead of as raw integers. 15 | This simplifies looking up related entries using Astro’s `getEntry()` and `getEntries()` functions. 16 | 17 | For example, post data contains an array specifying the categories that post belongs to. 18 | You can turn that array of references into a fully resolved array of category data using `getEntries()`: 19 | 20 | ```js 21 | import { getEntry, getEntries } from 'astro:content'; 22 | 23 | const myFirstPost = await getEntry('posts', '1'); 24 | // [{ id: '1', data: { name: 'Hello world!', categories: [{ id: '1', collection: 'categories' }], ... } }] 25 | const categories = await getEntries(myFirstPost.data.categories); 26 | // [{ id: '1', data: { name: 'Uncategorized', ... } }] 27 | ``` 28 | 29 | ## Collections 30 | 31 | Using `wpCollections()` in your content configuration adds the following content collections to your Astro project. 32 | 33 | ### `posts` 34 | 35 | The `posts` collection contains entries from WordPress’s [`wp/v2/posts` endpoint](https://developer.wordpress.org/rest-api/reference/posts/). 36 | These are the blog posts and include fully rendered post content. 37 | 38 | #### Example usage 39 | 40 | ```astro title="example-post.astro" 41 | --- 42 | import { getEntry, render } from 'astro:content'; 43 | const post = await getEntry('posts', '1'); 44 | // post = { 45 | // id: '1', 46 | // collection: 'posts', 47 | // data: { 48 | // title: { rendered: 'Hello world!' }, 49 | // date: Wed Jun 04 2008 50 | // // ... 51 | // }, 52 | // ... 53 | // } 54 | 55 | const { Content } = render(post); 56 | --- 57 | 58 |

59 | 60 | ``` 61 | 62 | 63 | 64 | ### `pages` 65 | 66 | The `pages` collection contains content from WordPress’s [`wp/v2/pages` endpoint](https://developer.wordpress.org/rest-api/reference/pages/). 67 | Pages include fully rendered content. 68 | 69 | Unlike posts, pages are hierarchical and one page can have a `parent` page, allowing for the creation of tree-like page relationships. 70 | 71 | #### Example usage 72 | 73 | ```astro title="example-page.astro" 74 | --- 75 | import { getEntry, render } from 'astro:content'; 76 | 77 | const page = await getEntry('pages', '49'); 78 | const { Content } = render(page); 79 | 80 | // Get the parent page if there is one: 81 | const parent = page.data.parent && await getEntry(page.data.parent); 82 | --- 83 | 84 |

85 | 86 | 87 | 88 | {parent && ( 89 |

90 | This page is a child of 91 | 92 | )} 93 | ``` 94 | 95 | 96 | 97 | ### `categories` 98 | 99 | The `categories` collection contains entries from WordPress’s [`wp/v2/categories` endpoint](https://developer.wordpress.org/rest-api/reference/categories/). 100 | Entries in the `posts` collection can contain references to categories. 101 | Metadata about individual categories such as their name, description, and number of posts are stored in the `categories` collection. 102 | 103 | 104 | 105 | ### `tags` 106 | 107 | The `tags` collection contains entries from WordPress’s [`wp/v2/tags` endpoint](https://developer.wordpress.org/rest-api/reference/tags/). 108 | Entries in the `posts` collection can contain references to tags. 109 | Metadata about individual tags such as their name, description, and number of posts are stored in the `tags` collection. 110 | 111 | 112 | 113 | ### `users` 114 | 115 | The `users` collection contains users from WordPress’s [`wp/v2/users`](https://developer.wordpress.org/rest-api/reference/users/) endpoint. 116 | 117 | 118 | 119 | ### `comments` 120 | 121 | The `comments` collection contains user comments from WordPress’s [`wp/v2/comments`](https://developer.wordpress.org/rest-api/reference/comments/) endpoint. 122 | 123 | 124 | 125 | ### `media` 126 | 127 | The `media` collection contains media attachments from WordPress’s [`wp/v2/media`](https://developer.wordpress.org/rest-api/reference/media/) endpoint. 128 | 129 | 130 | 131 | ### `statuses` 132 | 133 | The `statuses` collection contains status definitions from WordPress’s [`wp/v2/statuses`](https://developer.wordpress.org/rest-api/reference/post-statuses/) endpoint. 134 | 135 | 136 | 137 | ### `taxonomies` 138 | 139 | The `taxonomies` collection contains taxonomy definitions from WordPress’s [`wp/v2/taxonomies`](https://developer.wordpress.org/rest-api/reference/taxonomies/) endpoint. 140 | 141 | 142 | 143 | ### `types` 144 | 145 | The `types` collection contains post type definitions from WordPress’s [`wp/v2/types`](https://developer.wordpress.org/rest-api/reference/types/) endpoint. 146 | 147 | 148 | 149 | ### `site-settings` 150 | 151 | The `site-settings` collection contains a single entry with some basic configuration options exposed by WordPress. 152 | 153 | #### Example usage 154 | 155 | The easiest way to load site settings is with DeWP’s `getSiteSettings` utility: 156 | 157 | ```ts 158 | import { getSiteSettings } from 'dewp/content-utils'; 159 | 160 | const settings = await getSiteSettings(); 161 | ``` 162 | 163 | 164 | -------------------------------------------------------------------------------- /docs/src/content/docs/reference/utilities.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Utilities Reference 3 | description: Documentation of DeWP’s utility functions for working with content. 4 | --- 5 | 6 | This reference page documents utility functions provided by DeWP for working with your WordPress content in Astro. 7 | 8 | ## Content utilities 9 | 10 | Content utilities help simplify common operations with your WordPress content. 11 | 12 | These utilities can be imported from the `dewp/content-utils` module. 13 | 14 | ```ts 15 | import { getSiteSettings, resolvePageSlug } from 'dewp/content-utils'; 16 | ``` 17 | 18 | ### `getSiteSettings()` 19 | 20 | **Type:** `() => Promise>` 21 | 22 | Get settings from the [`site-settings`](/dewp/reference/collections/#site-settings) content collection. 23 | Includes `name` (the site title), `description`, and a couple of other handy bits of metadata. 24 | 25 | ```ts 26 | const { name, description } = await getSiteSettings(); 27 | ``` 28 | 29 | ### `resolvePageSlug(page)` 30 | 31 | **Type:** `(page: CollectionEntry<'pages'>) => Promise` 32 | 33 | If pages have parents, WordPress prepends parent slugs to the page slug. 34 | For example, given a `lion` page with a `big-cats` parent, the page would be served at `big-cats/lion`. 35 | 36 | The `resolvePageSlug()` function resolves parent pages to construct a multi-segment path in the same way. 37 | 38 | ```ts 39 | const lionPage = await getEntry('pages', '🦁'); 40 | console.log(lionPage.data.slug); // => "lion" 41 | 42 | const slug = await resolvePageSlug(lionPage); 43 | console.log(slug); // => "big-cats/lion" 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /docs/src/overrides/Head.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { Props } from '@astrojs/starlight/props'; 3 | import Default from '@astrojs/starlight/components/Head.astro'; 4 | 5 | // Get the URL of the generated image for the current page using its 6 | // ID and replace the file extension with `.png`. 7 | const ogImageUrl = new URL( 8 | `${import.meta.env.BASE_URL}/og/${Astro.props.id.replace(/\.\w+$/, '.png')}`, 9 | Astro.site 10 | ); 11 | --- 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/src/pages/og/[...slug].ts: -------------------------------------------------------------------------------- 1 | import { getCollection } from 'astro:content'; 2 | import { OGImageRoute } from 'astro-og-canvas'; 3 | 4 | // Get all entries from the `docs` content collection. 5 | const entries = await getCollection('docs'); 6 | 7 | // Map the entry array to an object with the page ID as key and the 8 | // frontmatter data as value. 9 | const pages = Object.fromEntries(entries.map(({ data, id }) => [id, { data }])); 10 | 11 | export const { getStaticPaths, GET } = OGImageRoute({ 12 | // Pass down the documentation pages. 13 | pages, 14 | // Define the name of the parameter used in the endpoint path, here `slug` 15 | // as the file is named `[...slug].ts`. 16 | param: 'slug', 17 | // Define a function called for each page to customize the generated image. 18 | getImageOptions: (_path, page: (typeof pages)[number]) => { 19 | return { 20 | // Use the page title and description as the image title and description. 21 | title: page.data.title, 22 | description: page.data.description || '', 23 | // Customize various colors and add a border. 24 | bgGradient: [[23, 24, 28] as const], 25 | border: { color: [179, 199, 255], width: 10, side: 'block-end' }, 26 | font: { 27 | title: { 28 | size: 90, 29 | color: [255, 255, 255], 30 | families: ['IBM Plex Sans'], 31 | weight: 'Bold', 32 | }, 33 | description: { 34 | color: [136, 140, 150], 35 | families: ['IBM Plex Sans'], 36 | lineHeight: 1.4, 37 | }, 38 | }, 39 | fonts: [ 40 | 'https://cdn.jsdelivr.net/fontsource/fonts/ibm-plex-sans@latest/latin-400-normal.woff2', 41 | 'https://cdn.jsdelivr.net/fontsource/fonts/ibm-plex-sans@latest/latin-700-normal.woff2', 42 | ], 43 | padding: 90, 44 | logo: { 45 | path: './src/pages/og/_logo.png', 46 | }, 47 | }; 48 | }, 49 | }); 50 | -------------------------------------------------------------------------------- /docs/src/pages/og/_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delucis/dewp/31304890bf4d44869f8ebc5a43a3be0a08bb1ebf/docs/src/pages/og/_logo.png -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strictest" 3 | } 4 | -------------------------------------------------------------------------------- /examples/blog-starter/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 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 | 23 | # jetbrains setting folder 24 | .idea/ 25 | -------------------------------------------------------------------------------- /examples/blog-starter/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /examples/blog-starter/.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 | -------------------------------------------------------------------------------- /examples/blog-starter/README.md: -------------------------------------------------------------------------------- 1 | # DeWP Starter Kit: Blog 2 | 3 | ```sh 4 | npm create astro@latest -- --template delucis/dewp/examples/blog-starter 5 | ``` 6 | 7 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/delucis/dewp/tree/main/examples/blog-starter) 8 | [![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/delucis/dewp/tree/main/examples/blog-starter) 9 | 10 | > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! 11 | 12 | ## 🚀 Project Structure 13 | 14 | Inside of your Astro project, you'll see the following folders and files: 15 | 16 | ```text 17 | ├── public/ 18 | ├── src/ 19 | │   ├── components/ 20 | │   ├── content/ 21 | │   ├── layouts/ 22 | │   └── pages/ 23 | ├── astro.config.mjs 24 | ├── README.md 25 | ├── package.json 26 | └── tsconfig.json 27 | ``` 28 | 29 | Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. 30 | 31 | There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. 32 | 33 | The `src/content/config.ts` file configures "collections" of data loaded from WordPress. Use `getCollection()` to retrieve blog posts and other content. See [Astro's Content Collections docs](https://docs.astro.build/en/guides/content-collections/) to learn more. 34 | 35 | Any static assets, like images, can be placed in the `public/` directory. 36 | 37 | ## 🧞 Commands 38 | 39 | All commands are run from the root of the project, from a terminal: 40 | 41 | | Command | Action | 42 | | :------------------------ | :----------------------------------------------- | 43 | | `npm install` | Installs dependencies | 44 | | `npm run dev` | Starts local dev server at `localhost:4321` | 45 | | `npm run build` | Build your production site to `./dist/` | 46 | | `npm run preview` | Preview your build locally, before deploying | 47 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | 48 | | `npm run astro -- --help` | Get help using the Astro CLI | 49 | 50 | ## 👀 Want to learn more? 51 | 52 | Check out [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). 53 | 54 | ## Credit 55 | 56 | This theme is based off of the lovely [Bear Blog](https://github.com/HermanMartinus/bearblog/). 57 | -------------------------------------------------------------------------------- /examples/blog-starter/astro.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import sitemap from '@astrojs/sitemap'; 3 | import { defineConfig } from 'astro/config'; 4 | 5 | // https://astro.build/config 6 | export default defineConfig({ 7 | site: 'https://example.com', 8 | integrations: [sitemap()], 9 | }); 10 | -------------------------------------------------------------------------------- /examples/blog-starter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@examples/blog-starter", 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 | }, 13 | "dependencies": { 14 | "@astrojs/rss": "^4.0.8", 15 | "@astrojs/sitemap": "^3.2.0", 16 | "astro": "^5.0.8", 17 | "dewp": "^0.0.6" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/blog-starter/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /examples/blog-starter/public/fonts/atkinson-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delucis/dewp/31304890bf4d44869f8ebc5a43a3be0a08bb1ebf/examples/blog-starter/public/fonts/atkinson-bold.woff -------------------------------------------------------------------------------- /examples/blog-starter/public/fonts/atkinson-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delucis/dewp/31304890bf4d44869f8ebc5a43a3be0a08bb1ebf/examples/blog-starter/public/fonts/atkinson-regular.woff -------------------------------------------------------------------------------- /examples/blog-starter/src/components/BaseHead.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // Import the global.css file here so that it is included on 3 | // all pages through the use of the component. 4 | import '../styles/global.css'; 5 | 6 | interface Props { 7 | /** Title of the current page used in SEO metadata like the `` tag. */ 8 | title: string; 9 | /** Description of the current page used in SEO metadata like the `<meta name="description">` tag. */ 10 | description: string; 11 | /** Optional open graph image for the current page. */ 12 | image?: string | undefined; 13 | } 14 | 15 | const canonicalURL = new URL(Astro.url.pathname, Astro.site); 16 | 17 | const { title, description, image } = Astro.props; 18 | --- 19 | 20 | <!-- Global Metadata --> 21 | <meta charset="utf-8" /> 22 | <meta name="viewport" content="width=device-width,initial-scale=1" /> 23 | <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> 24 | <meta name="generator" content={Astro.generator} /> 25 | 26 | <!-- Font preloads --> 27 | <link rel="preload" href="/fonts/atkinson-regular.woff" as="font" type="font/woff" crossorigin /> 28 | <link rel="preload" href="/fonts/atkinson-bold.woff" as="font" type="font/woff" crossorigin /> 29 | 30 | <!-- Canonical URL --> 31 | <link rel="canonical" href={canonicalURL} /> 32 | 33 | <!-- Primary Meta Tags --> 34 | <title set:html={title} /> 35 | <meta name="title" content={title} /> 36 | <meta name="description" content={description} /> 37 | 38 | <!-- Open Graph / Facebook --> 39 | <meta property="og:type" content="website" /> 40 | <meta property="og:url" content={Astro.url} /> 41 | <meta property="og:title" content={title} /> 42 | <meta property="og:description" content={description} /> 43 | {image && <meta property="og:image" content={new URL(image, Astro.url)} />} 44 | 45 | <!-- Twitter --> 46 | <meta property="twitter:card" content="summary_large_image" /> 47 | <meta property="twitter:url" content={Astro.url} /> 48 | <meta property="twitter:title" content={title} /> 49 | <meta property="twitter:description" content={description} /> 50 | {image && <meta property="twitter:image" content={new URL(image, Astro.url)} />} 51 | -------------------------------------------------------------------------------- /examples/blog-starter/src/components/Comments.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getCollection } from 'astro:content'; 3 | 4 | interface Props { 5 | id: number; 6 | } 7 | 8 | const { id } = Astro.props; 9 | const comments = await getCollection('comments', (comment) => comment.data.post?.id === String(id)); 10 | --- 11 | 12 | { 13 | comments.length > 0 && ( 14 | <section> 15 | <h2>Comments ({comments.length})</h2> 16 | <ol class="comments"> 17 | {comments 18 | .sort((a, b) => (a.data.date < b.data.date ? -1 : 1)) 19 | .map((comment) => ( 20 | <li class="comment" id={`comment-${comment.data.id}`}> 21 | <header class="comment-header"> 22 | <img 23 | class="comment-author-avatar" 24 | src={comment.data.author_avatar_urls[96]} 25 | width="32" 26 | height="32" 27 | alt="" 28 | /> 29 | <div class="comment-byline"> 30 | <div class="comment-author-name"> 31 | {comment.data.author_url ? ( 32 | <a href={comment.data.author_url}>{comment.data.author_name}</a> 33 | ) : ( 34 | <span>{comment.data.author_name}</span> 35 | )} 36 | </div> 37 | <div> 38 | <a class="comment-date" href={`#comment-${comment.data.id}`}> 39 | <time datetime={comment.data.date.toISOString()}> 40 | {comment.data.date.toLocaleDateString('en-US', { dateStyle: 'medium' })} 41 | </time> 42 | </a> 43 | </div> 44 | </div> 45 | </header> 46 | <div set:html={comment.data.content.rendered} /> 47 | </li> 48 | ))} 49 | </ol> 50 | </section> 51 | ) 52 | } 53 | 54 | <style> 55 | .comment-header { 56 | display: flex; 57 | gap: 1rem; 58 | align-items: center; 59 | } 60 | .comments { 61 | padding: 0; 62 | list-style: none; 63 | } 64 | .comment-author-avatar { 65 | width: 3.5rem; 66 | height: 3.5rem; 67 | border-radius: 999rem; 68 | } 69 | .comment-byline { 70 | display: flex; 71 | flex-direction: column; 72 | line-height: 1.4; 73 | } 74 | .comment-author-name { 75 | font-weight: 700; 76 | } 77 | .comment-date { 78 | color: rgba(var(--gray-dark), 0.7); 79 | } 80 | </style> 81 | -------------------------------------------------------------------------------- /examples/blog-starter/src/components/Footer.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const today = new Date(); 3 | --- 4 | 5 | <footer> 6 | © {today.getFullYear()} Your name here. All rights reserved. 7 | <div class="social-links"> 8 | <a href="https://m.webtoo.ls/@astro" target="_blank"> 9 | <span class="sr-only">Follow Astro on Mastodon</span> 10 | <svg 11 | viewBox="0 0 16 16" 12 | aria-hidden="true" 13 | width="32" 14 | height="32" 15 | astro-icon="social/mastodon" 16 | ><path 17 | fill="currentColor" 18 | d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z" 19 | ></path></svg 20 | > 21 | </a> 22 | <a href="https://twitter.com/astrodotbuild" target="_blank"> 23 | <span class="sr-only">Follow Astro on Twitter</span> 24 | <svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32" astro-icon="social/twitter" 25 | ><path 26 | fill="currentColor" 27 | d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15z" 28 | ></path></svg 29 | > 30 | </a> 31 | <a href="https://github.com/withastro/astro" target="_blank"> 32 | <span class="sr-only">Go to Astro's GitHub repo</span> 33 | <svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32" astro-icon="social/github" 34 | ><path 35 | fill="currentColor" 36 | d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z" 37 | ></path></svg 38 | > 39 | </a> 40 | </div> 41 | </footer> 42 | <style> 43 | footer { 44 | padding: 2em 1em 6em 1em; 45 | background: linear-gradient(var(--gray-gradient)) no-repeat; 46 | color: rgb(var(--gray)); 47 | text-align: center; 48 | } 49 | .social-links { 50 | display: flex; 51 | justify-content: center; 52 | gap: 1em; 53 | margin-top: 1em; 54 | } 55 | .social-links a { 56 | text-decoration: none; 57 | color: rgb(var(--gray)); 58 | } 59 | .social-links a:hover { 60 | color: rgb(var(--gray-dark)); 61 | } 62 | </style> 63 | -------------------------------------------------------------------------------- /examples/blog-starter/src/components/FormattedDate.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | date: Date; 4 | } 5 | 6 | const { date } = Astro.props; 7 | --- 8 | 9 | <time datetime={date.toISOString()}> 10 | { 11 | date.toLocaleDateString('en-us', { 12 | year: 'numeric', 13 | month: 'short', 14 | day: 'numeric', 15 | }) 16 | } 17 | </time> 18 | -------------------------------------------------------------------------------- /examples/blog-starter/src/components/Header.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getSiteSettings } from 'dewp/content-utils'; 3 | import HeaderLink from './HeaderLink.astro'; 4 | 5 | const { name } = await getSiteSettings(); 6 | --- 7 | 8 | <header> 9 | <nav> 10 | <h2><a href="/">{name}</a></h2> 11 | <div class="internal-links"> 12 | <HeaderLink href="/">Home</HeaderLink> 13 | <HeaderLink href="/blog">Blog</HeaderLink> 14 | </div> 15 | <div class="social-links"> 16 | <a href="https://m.webtoo.ls/@astro" target="_blank"> 17 | <span class="sr-only">Follow Astro on Mastodon</span> 18 | <svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32"> 19 | <path 20 | fill="currentColor" 21 | d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z" 22 | ></path> 23 | </svg> 24 | </a> 25 | <a href="https://twitter.com/astrodotbuild" target="_blank"> 26 | <span class="sr-only">Follow Astro on Twitter</span> 27 | <svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32"> 28 | <path 29 | fill="currentColor" 30 | d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15z" 31 | ></path> 32 | </svg> 33 | </a> 34 | <a href="https://github.com/withastro/astro" target="_blank"> 35 | <span class="sr-only">Go to Astro's GitHub repo</span> 36 | <svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32"> 37 | <path 38 | fill="currentColor" 39 | d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z" 40 | ></path> 41 | </svg> 42 | </a> 43 | </div> 44 | </nav> 45 | </header> 46 | <style> 47 | header { 48 | margin: 0; 49 | padding: 0 1em; 50 | background: white; 51 | box-shadow: 0 2px 8px rgba(var(--black), 5%); 52 | } 53 | h2 { 54 | margin: 0; 55 | font-size: 1em; 56 | } 57 | 58 | h2 a, 59 | h2 a.active { 60 | text-decoration: none; 61 | } 62 | nav { 63 | display: flex; 64 | align-items: center; 65 | justify-content: space-between; 66 | } 67 | nav a { 68 | padding: 1em 0.5em; 69 | color: var(--black); 70 | border-bottom: 4px solid transparent; 71 | text-decoration: none; 72 | } 73 | nav a.active { 74 | text-decoration: none; 75 | border-bottom-color: var(--accent); 76 | } 77 | .social-links, 78 | .social-links a { 79 | display: flex; 80 | } 81 | @media (max-width: 720px) { 82 | .social-links { 83 | display: none; 84 | } 85 | } 86 | </style> 87 | -------------------------------------------------------------------------------- /examples/blog-starter/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 | 8 | const { pathname } = Astro.url; 9 | const subpath = pathname.match(/[^\/]+/g); 10 | const isActive = href === pathname || href === '/' + subpath?.[0]; 11 | --- 12 | 13 | <a href={href} class:list={[className, { active: isActive }]} {...props}> 14 | <slot /> 15 | </a> 16 | <style> 17 | a { 18 | display: inline-block; 19 | text-decoration: none; 20 | } 21 | a.active { 22 | font-weight: bolder; 23 | text-decoration: underline; 24 | } 25 | </style> 26 | -------------------------------------------------------------------------------- /examples/blog-starter/src/components/PageIndex.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // This component builds a tree structure from the WordPress pages collection. 3 | // It uses each page’s `parent` property (if set) to create a hierarchy and show links to all pages. 4 | 5 | import { getCollection, getEntry, type CollectionEntry } from 'astro:content'; 6 | import PageTree from './PageTree.astro'; 7 | 8 | type PageMap = Map<string, { parent: CollectionEntry<'pages'>; children: PageMap }>; 9 | const tree: PageMap = new Map(); 10 | 11 | const pages = await getCollection('pages'); 12 | 13 | for (const page of pages) { 14 | // Build up a path of “breadcrumbs“ to the current page (i.e. a list of parents). 15 | const breadcrumbs = [page]; 16 | let parentReference = page.data.parent; 17 | while (parentReference) { 18 | const resolvedParent = await getEntry(parentReference); 19 | parentReference = resolvedParent.data.parent; 20 | breadcrumbs.unshift(resolvedParent); 21 | } 22 | // Use the breadcrumbs to add this page at the correct position in the tree. 23 | let branch = tree; 24 | for (const crumb of breadcrumbs) { 25 | const node = branch.get(crumb.id) || { parent: crumb, children: new Map() }; 26 | branch.set(crumb.id, node); 27 | branch = node.children; 28 | } 29 | } 30 | --- 31 | 32 | <PageTree {tree} /> 33 | -------------------------------------------------------------------------------- /examples/blog-starter/src/components/PageTree.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // Recursive component that renders a tree structure of page links. 3 | 4 | import type { CollectionEntry } from 'astro:content'; 5 | import { resolvePageSlug } from 'dewp/content-utils'; 6 | 7 | type PageMap = Map<string, { parent: CollectionEntry<'pages'>; children: PageMap }>; 8 | 9 | interface Props { 10 | tree: PageMap; 11 | } 12 | 13 | const tree = [...Astro.props.tree.values()]; 14 | --- 15 | 16 | <ul> 17 | { 18 | tree.map(async ({ parent, children }) => ( 19 | <li> 20 | <a href={`/${await resolvePageSlug(parent)}`} set:html={parent.data.title.rendered} /> 21 | <Astro.self tree={children} /> 22 | </li> 23 | )) 24 | } 25 | </ul> 26 | -------------------------------------------------------------------------------- /examples/blog-starter/src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { wpCollections } from 'dewp/loaders'; 2 | 3 | export const collections = wpCollections({ 4 | endpoint: 'https://wordpress.org/news/wp-json/', 5 | }); 6 | -------------------------------------------------------------------------------- /examples/blog-starter/src/layouts/BaseLayout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { ComponentProps } from 'astro/types'; 3 | import BaseHead from '../components/BaseHead.astro'; 4 | import Footer from '../components/Footer.astro'; 5 | import Header from '../components/Header.astro'; 6 | 7 | type Props = ComponentProps<typeof BaseHead>; 8 | 9 | const { title, description, image } = Astro.props; 10 | --- 11 | 12 | <!doctype html> 13 | <html lang="en"> 14 | <head> 15 | <BaseHead {title} {description} {image} /> 16 | </head> 17 | <body> 18 | <Header /> 19 | <slot /> 20 | <Footer /> 21 | </body> 22 | </html> 23 | -------------------------------------------------------------------------------- /examples/blog-starter/src/layouts/BlogPost.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getEntries, getEntry, type CollectionEntry } from 'astro:content'; 3 | import FormattedDate from '../components/FormattedDate.astro'; 4 | import BaseLayout from './BaseLayout.astro'; 5 | import Comments from '../components/Comments.astro'; 6 | 7 | type Props = CollectionEntry<'posts'>['data']; 8 | 9 | const { id, title, excerpt, date, modified, featured_media } = Astro.props; 10 | 11 | const tags = await getEntries(Astro.props.tags); 12 | const categories = await getEntries(Astro.props.categories); 13 | const author = await getEntry(Astro.props.author); 14 | const heroImage = featured_media && (await getEntry(featured_media)); 15 | --- 16 | 17 | <BaseLayout title={title.rendered} description={excerpt.rendered}> 18 | <main> 19 | <article> 20 | <div class="hero-image"> 21 | { 22 | heroImage && heroImage.data.media_type === 'image' && ( 23 | <img width={1020} height={510} src={heroImage.data.source_url} alt="" /> 24 | ) 25 | } 26 | </div> 27 | <div class="prose"> 28 | <div class="title"> 29 | <div class="date"> 30 | <FormattedDate date={date} /> 31 | { 32 | modified && ( 33 | <div class="last-updated-on"> 34 | Last updated on <FormattedDate date={modified} /> 35 | </div> 36 | ) 37 | } 38 | </div> 39 | <h1 set:html={title.rendered} /> 40 | <p> 41 | By {author?.data?.name || 'Unknown Author'} 42 | { 43 | categories.length > 0 && ( 44 | <> 45 | in{' '} 46 | {categories.map((category, i, { length }) => ( 47 | <> 48 | <a href={`/category/${category.data.slug}`}>{category.data.name}</a> 49 | {i < length - 1 && ','} 50 | </> 51 | ))} 52 | </> 53 | ) 54 | } 55 | </p> 56 | <hr /> 57 | </div> 58 | 59 | <slot /> 60 | 61 | { 62 | tags.length > 0 && ( 63 | <> 64 | <hr /> 65 | <p> 66 | <strong>Tags:</strong>{' '} 67 | {tags.map((tag, i, { length }) => ( 68 | <> 69 | <a href={`/tag/${tag.data.slug}`}>{tag.data.name}</a> 70 | {i < length - 1 && '/'} 71 | </> 72 | ))} 73 | </p> 74 | </> 75 | ) 76 | } 77 | <Comments {id} /> 78 | </div> 79 | </article> 80 | </main> 81 | </BaseLayout> 82 | 83 | <style> 84 | main { 85 | width: calc(100% - 2em); 86 | max-width: 100%; 87 | margin: 0; 88 | } 89 | .hero-image { 90 | width: 100%; 91 | } 92 | .hero-image img { 93 | display: block; 94 | object-fit: cover; 95 | aspect-ratio: 2; 96 | margin: 0 auto; 97 | border-radius: 12px; 98 | box-shadow: var(--box-shadow); 99 | } 100 | .prose { 101 | width: 720px; 102 | max-width: calc(100% - 2em); 103 | margin: auto; 104 | padding: 1em; 105 | color: rgb(var(--gray-dark)); 106 | } 107 | .title { 108 | margin-bottom: 1em; 109 | padding: 1em 0; 110 | text-align: center; 111 | line-height: 1; 112 | } 113 | .title h1 { 114 | margin: 0 0 0.5em 0; 115 | } 116 | .date { 117 | margin-bottom: 0.5em; 118 | color: rgb(var(--gray)); 119 | } 120 | .last-updated-on { 121 | font-style: italic; 122 | } 123 | </style> 124 | -------------------------------------------------------------------------------- /examples/blog-starter/src/layouts/Category.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getCollection, getEntry, type CollectionEntry } from 'astro:content'; 3 | import FormattedDate from '../components/FormattedDate.astro'; 4 | import BaseLayout from './BaseLayout.astro'; 5 | 6 | type Props = CollectionEntry<'categories'>['data']; 7 | 8 | const { id, name, description, count } = Astro.props; 9 | 10 | // Get blog posts with the current category 11 | const posts = await getCollection('posts', (post) => 12 | post.data.categories.find((cat) => cat.id == id) 13 | ); 14 | --- 15 | 16 | <BaseLayout title={name} description={description}> 17 | <main> 18 | <article> 19 | <div class="prose"> 20 | <div class="title"> 21 | <h1>{name}</h1> 22 | <p>{count} post{count !== 1 && 's'} in {name}</p> 23 | <hr /> 24 | </div> 25 | <ul> 26 | { 27 | posts.map(async (post) => { 28 | const heroImage = 29 | post.data.featured_media && (await getEntry(post.data.featured_media)); 30 | return ( 31 | <li> 32 | <a href={`/blog/${post.data.slug}/`}> 33 | {heroImage && ( 34 | <img width={720} height={360} src={heroImage?.data.source_url} alt="" /> 35 | )} 36 | <h4 class="title" set:html={post.data.title.rendered} /> 37 | </a> 38 | <p class="date"> 39 | <FormattedDate date={post.data.date} /> 40 | </p> 41 | </li> 42 | ); 43 | }) 44 | } 45 | </ul> 46 | <hr /> 47 | </div> 48 | </article> 49 | </main> 50 | </BaseLayout> 51 | 52 | <style> 53 | main { 54 | width: 960px; 55 | } 56 | ul { 57 | display: flex; 58 | flex-wrap: wrap; 59 | gap: 2rem; 60 | list-style-type: none; 61 | margin: 0; 62 | padding: 0; 63 | } 64 | ul li { 65 | width: calc(50% - 1rem); 66 | } 67 | ul li * { 68 | text-decoration: none; 69 | transition: 0.2s ease; 70 | } 71 | ul li:first-child { 72 | width: 100%; 73 | margin-bottom: 1rem; 74 | text-align: center; 75 | } 76 | ul li:first-child img { 77 | width: 100%; 78 | } 79 | ul li:first-child .title { 80 | font-size: 2.369rem; 81 | } 82 | ul li img { 83 | margin-bottom: 0.5rem; 84 | border-radius: 12px; 85 | aspect-ratio: 2; 86 | object-fit: cover; 87 | } 88 | ul li a { 89 | display: block; 90 | } 91 | .title { 92 | margin: 0; 93 | color: rgb(var(--black)); 94 | line-height: 1; 95 | } 96 | .date { 97 | margin: 0; 98 | color: rgb(var(--gray)); 99 | } 100 | ul li a:hover h4, 101 | ul li a:hover .date { 102 | color: rgb(var(--accent)); 103 | } 104 | ul a:hover img { 105 | box-shadow: var(--box-shadow); 106 | } 107 | @media (max-width: 720px) { 108 | ul { 109 | gap: 0.5em; 110 | } 111 | ul li { 112 | width: 100%; 113 | text-align: center; 114 | } 115 | ul li:first-child { 116 | margin-bottom: 0; 117 | } 118 | ul li:first-child .title { 119 | font-size: 1.563em; 120 | } 121 | } 122 | </style> 123 | -------------------------------------------------------------------------------- /examples/blog-starter/src/layouts/Page.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getEntry, type CollectionEntry } from 'astro:content'; 3 | import FormattedDate from '../components/FormattedDate.astro'; 4 | import BaseLayout from './BaseLayout.astro'; 5 | 6 | type Props = CollectionEntry<'pages'>['data']; 7 | 8 | const { title, excerpt, date, modified } = Astro.props; 9 | const featuredMedia = Astro.props.featured_media && (await getEntry(Astro.props.featured_media)); 10 | --- 11 | 12 | <BaseLayout title={title.rendered} description={excerpt.rendered}> 13 | <main> 14 | <article> 15 | <div class="hero-image"> 16 | { 17 | featuredMedia && featuredMedia.data.media_type === 'image' && ( 18 | <img width={1020} height={510} src={featuredMedia.data.source_url} alt="" /> 19 | ) 20 | } 21 | </div> 22 | <div class="prose"> 23 | <div class="title"> 24 | <div class="date"> 25 | <FormattedDate date={date} /> 26 | { 27 | modified && ( 28 | <div class="last-updated-on"> 29 | Last updated on <FormattedDate date={modified} /> 30 | </div> 31 | ) 32 | } 33 | </div> 34 | <h1 set:html={title.rendered} /> 35 | <hr /> 36 | </div> 37 | <slot /> 38 | </div> 39 | </article> 40 | </main> 41 | </BaseLayout> 42 | 43 | <style> 44 | main { 45 | width: calc(100% - 2em); 46 | max-width: 100%; 47 | margin: 0; 48 | } 49 | .hero-image { 50 | width: 100%; 51 | } 52 | .hero-image img { 53 | display: block; 54 | object-fit: cover; 55 | aspect-ratio: 2; 56 | margin: 0 auto; 57 | border-radius: 12px; 58 | box-shadow: var(--box-shadow); 59 | } 60 | .prose { 61 | width: 720px; 62 | max-width: calc(100% - 2em); 63 | margin: auto; 64 | padding: 1em; 65 | color: rgb(var(--gray-dark)); 66 | } 67 | .title { 68 | margin-bottom: 1em; 69 | padding: 1em 0; 70 | text-align: center; 71 | line-height: 1; 72 | } 73 | .title h1 { 74 | margin: 0 0 0.5em 0; 75 | } 76 | .date { 77 | margin-bottom: 0.5em; 78 | color: rgb(var(--gray)); 79 | } 80 | .last-updated-on { 81 | font-style: italic; 82 | } 83 | </style> 84 | -------------------------------------------------------------------------------- /examples/blog-starter/src/layouts/Tag.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getCollection, getEntry, type CollectionEntry } from 'astro:content'; 3 | import FormattedDate from '../components/FormattedDate.astro'; 4 | import BaseLayout from './BaseLayout.astro'; 5 | 6 | type Props = CollectionEntry<'tags'>['data']; 7 | 8 | const { id, name, description, count } = Astro.props; 9 | 10 | // Get blog posts with the current category 11 | const posts = await getCollection('posts', (post) => post.data.tags.find((tag) => tag.id == id)); 12 | --- 13 | 14 | <BaseLayout title={name} description={description}> 15 | <main> 16 | <article> 17 | <div class="prose"> 18 | <div class="title"> 19 | <h1>{name}</h1> 20 | <p>{count} post{count !== 1 && 's'} tagged {name}</p> 21 | <hr /> 22 | </div> 23 | <ul> 24 | { 25 | posts.map(async (post) => { 26 | const heroImage = 27 | post.data.featured_media && (await getEntry(post.data.featured_media)); 28 | return ( 29 | <li> 30 | <a href={`/blog/${post.data.slug}/`}> 31 | {heroImage && ( 32 | <img width={720} height={360} src={heroImage?.data.source_url} alt="" /> 33 | )} 34 | <h4 class="title" set:html={post.data.title.rendered} /> 35 | </a> 36 | <p class="date"> 37 | <FormattedDate date={post.data.date} /> 38 | </p> 39 | </li> 40 | ); 41 | }) 42 | } 43 | </ul> 44 | <hr /> 45 | </div> 46 | </article> 47 | </main> 48 | </BaseLayout> 49 | 50 | <style> 51 | main { 52 | width: 960px; 53 | } 54 | ul { 55 | display: flex; 56 | flex-wrap: wrap; 57 | gap: 2rem; 58 | list-style-type: none; 59 | margin: 0; 60 | padding: 0; 61 | } 62 | ul li { 63 | width: calc(50% - 1rem); 64 | } 65 | ul li * { 66 | text-decoration: none; 67 | transition: 0.2s ease; 68 | } 69 | ul li:first-child { 70 | width: 100%; 71 | margin-bottom: 1rem; 72 | text-align: center; 73 | } 74 | ul li:first-child img { 75 | width: 100%; 76 | } 77 | ul li:first-child .title { 78 | font-size: 2.369rem; 79 | } 80 | ul li img { 81 | margin-bottom: 0.5rem; 82 | border-radius: 12px; 83 | aspect-ratio: 2; 84 | object-fit: cover; 85 | } 86 | ul li a { 87 | display: block; 88 | } 89 | .title { 90 | margin: 0; 91 | color: rgb(var(--black)); 92 | line-height: 1; 93 | } 94 | .date { 95 | margin: 0; 96 | color: rgb(var(--gray)); 97 | } 98 | ul li a:hover h4, 99 | ul li a:hover .date { 100 | color: rgb(var(--accent)); 101 | } 102 | ul a:hover img { 103 | box-shadow: var(--box-shadow); 104 | } 105 | @media (max-width: 720px) { 106 | ul { 107 | gap: 0.5em; 108 | } 109 | ul li { 110 | width: 100%; 111 | text-align: center; 112 | } 113 | ul li:first-child { 114 | margin-bottom: 0; 115 | } 116 | ul li:first-child .title { 117 | font-size: 1.563em; 118 | } 119 | } 120 | </style> 121 | -------------------------------------------------------------------------------- /examples/blog-starter/src/pages/[...slug].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getCollection, render } from 'astro:content'; 3 | import { resolvePageSlug } from 'dewp/content-utils'; 4 | import Page from '../layouts/Page.astro'; 5 | 6 | export async function getStaticPaths() { 7 | const pages = await getCollection('pages'); 8 | 9 | return await Promise.all( 10 | pages.map(async (page) => ({ 11 | params: { slug: await resolvePageSlug(page) }, 12 | props: page, 13 | })) 14 | ); 15 | } 16 | 17 | const page = Astro.props; 18 | const { Content } = await render(page); 19 | --- 20 | 21 | <Page {...page.data}> 22 | <Content /> 23 | </Page> 24 | -------------------------------------------------------------------------------- /examples/blog-starter/src/pages/blog/[...slug].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getCollection, render } from 'astro:content'; 3 | import BlogPost from '../../layouts/BlogPost.astro'; 4 | 5 | export async function getStaticPaths() { 6 | const posts = await getCollection('posts'); 7 | return posts.map((post) => ({ 8 | params: { slug: post.data.slug }, 9 | props: post, 10 | })); 11 | } 12 | 13 | const post = Astro.props; 14 | const { Content } = await render(post); 15 | --- 16 | 17 | <BlogPost {...post.data}> 18 | <Content /> 19 | </BlogPost> 20 | -------------------------------------------------------------------------------- /examples/blog-starter/src/pages/blog/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getCollection, getEntry } from 'astro:content'; 3 | import { getSiteSettings } from 'dewp/content-utils'; 4 | import FormattedDate from '../../components/FormattedDate.astro'; 5 | import BaseLayout from '../../layouts/BaseLayout.astro'; 6 | 7 | const { name, description } = await getSiteSettings(); 8 | 9 | const posts = (await getCollection('posts')) 10 | .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()) 11 | .slice(0, 10); 12 | --- 13 | 14 | <BaseLayout title={name} description={description}> 15 | <main> 16 | <section> 17 | <ul> 18 | { 19 | posts.map(async (post) => { 20 | const heroImage = 21 | post.data.featured_media && (await getEntry(post.data.featured_media)); 22 | return ( 23 | <li> 24 | <a href={`/blog/${post.data.slug}/`}> 25 | {heroImage && ( 26 | <img width={720} height={360} src={heroImage?.data.source_url} alt="" /> 27 | )} 28 | <h4 class="title" set:html={post.data.title.rendered} /> 29 | </a> 30 | <p class="date"> 31 | <FormattedDate date={post.data.date} /> 32 | </p> 33 | </li> 34 | ); 35 | }) 36 | } 37 | </ul> 38 | </section> 39 | </main> 40 | </BaseLayout> 41 | 42 | <style> 43 | main { 44 | width: 960px; 45 | } 46 | ul { 47 | display: flex; 48 | flex-wrap: wrap; 49 | gap: 2rem; 50 | list-style-type: none; 51 | margin: 0; 52 | padding: 0; 53 | } 54 | ul li { 55 | width: calc(50% - 1rem); 56 | } 57 | ul li * { 58 | text-decoration: none; 59 | transition: 0.2s ease; 60 | } 61 | ul li:first-child { 62 | width: 100%; 63 | margin-bottom: 1rem; 64 | text-align: center; 65 | } 66 | ul li:first-child img { 67 | width: 100%; 68 | } 69 | ul li:first-child .title { 70 | font-size: 2.369rem; 71 | } 72 | ul li img { 73 | margin-bottom: 0.5rem; 74 | border-radius: 12px; 75 | aspect-ratio: 2; 76 | object-fit: cover; 77 | } 78 | ul li a { 79 | display: block; 80 | } 81 | .title { 82 | margin: 0; 83 | color: rgb(var(--black)); 84 | line-height: 1; 85 | } 86 | .date { 87 | margin: 0; 88 | color: rgb(var(--gray)); 89 | } 90 | ul li a:hover h4, 91 | ul li a:hover .date { 92 | color: rgb(var(--accent)); 93 | } 94 | ul a:hover img { 95 | box-shadow: var(--box-shadow); 96 | } 97 | @media (max-width: 720px) { 98 | ul { 99 | gap: 0.5em; 100 | } 101 | ul li { 102 | width: 100%; 103 | text-align: center; 104 | } 105 | ul li:first-child { 106 | margin-bottom: 0; 107 | } 108 | ul li:first-child .title { 109 | font-size: 1.563em; 110 | } 111 | } 112 | </style> 113 | -------------------------------------------------------------------------------- /examples/blog-starter/src/pages/category/[slug].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getCollection } from 'astro:content'; 3 | import Category from '../../layouts/Category.astro'; 4 | 5 | export async function getStaticPaths() { 6 | const categories = await getCollection('categories'); 7 | 8 | return await Promise.all( 9 | categories.map(async (category) => ({ 10 | params: { slug: category.data.slug }, 11 | props: category, 12 | })) 13 | ); 14 | } 15 | 16 | const category = Astro.props; 17 | --- 18 | 19 | <Category {...category.data} /> 20 | -------------------------------------------------------------------------------- /examples/blog-starter/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getSiteSettings } from 'dewp/content-utils'; 3 | import PageIndex from '../components/PageIndex.astro'; 4 | import BaseLayout from '../layouts/BaseLayout.astro'; 5 | 6 | const { name, description, url } = await getSiteSettings(); 7 | --- 8 | 9 | <BaseLayout title={name} description={description}> 10 | <main> 11 | <h1>🧑‍🚀 Hello, Astronaut!</h1> 12 | <p> 13 | Welcome to the <a href="https://github.com/delucis/dewp">DeWP</a> blog starter template for Astro, 14 | which loads content from an existing WordPress website. 15 | </p> 16 | <p> 17 | Currently this site is loading its content from <a href={url}><code>{url}</code></a>. 18 | </p> 19 | <p> 20 | You can configure the content source in the <code>src/content/config.ts</code> file. 21 | </p> 22 | <p>Here are a few ideas on how to get started with the template:</p> 23 | <ul> 24 | <li>Edit this page in <code>src/pages/index.astro</code></li> 25 | <li>Edit the site header items in <code>src/components/Header.astro</code></li> 26 | <li>Add your name to the footer in <code>src/components/Footer.astro</code></li> 27 | <li>Customize the blog post page layout in <code>src/layouts/BlogPost.astro</code></li> 28 | </ul> 29 | <p> 30 | Have fun! If you get stuck, <a href="https://github.com/delucis/dewp">reach out on GitHub</a> or 31 | <a href="https://astro.build/chat">join us on Discord</a> to ask questions. 32 | </p> 33 | 34 | <h2>Page Index</h2> 35 | <p>Here is an overview of all the WordPress pages in your site:</p> 36 | <PageIndex /> 37 | </main> 38 | </BaseLayout> 39 | -------------------------------------------------------------------------------- /examples/blog-starter/src/pages/rss.xml.ts: -------------------------------------------------------------------------------- 1 | import rss from '@astrojs/rss'; 2 | import type { APIContext } from 'astro'; 3 | import { getCollection } from 'astro:content'; 4 | import { getSiteSettings } from 'dewp/content-utils'; 5 | 6 | export async function GET(context: APIContext) { 7 | const { name, description } = await getSiteSettings(); 8 | const posts = await getCollection('posts'); 9 | return rss({ 10 | title: name, 11 | description: description, 12 | site: context.site || 'https://example.com', 13 | items: posts 14 | .sort((a, b) => (a.data.date < b.data.date ? 1 : a.data.date > b.data.date ? -1 : 0)) 15 | .slice(0, 10) 16 | .map((post) => { 17 | return { 18 | link: `/blog/${post.data.id}/`, 19 | title: post.data.title.rendered, 20 | description: post.data.excerpt.rendered, 21 | pubDate: post.data.date, 22 | content: post.rendered?.html, 23 | }; 24 | }), 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /examples/blog-starter/src/pages/tag/[slug].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getCollection } from 'astro:content'; 3 | import Tag from '../../layouts/Tag.astro'; 4 | 5 | export async function getStaticPaths() { 6 | const tags = await getCollection('tags'); 7 | 8 | return await Promise.all( 9 | tags.map(async (tag) => ({ 10 | params: { slug: tag.data.slug }, 11 | props: tag, 12 | })) 13 | ); 14 | } 15 | 16 | const category = Astro.props; 17 | --- 18 | 19 | <Tag {...category.data} /> 20 | -------------------------------------------------------------------------------- /examples/blog-starter/src/styles/global.css: -------------------------------------------------------------------------------- 1 | /* 2 | The CSS in this style tag is based off of Bear Blog's default CSS. 3 | https://github.com/HermanMartinus/bearblog/blob/297026a877bc2ab2b3bdfbd6b9f7961c350917dd/templates/styles/blog/default.css 4 | License MIT: https://github.com/HermanMartinus/bearblog/blob/master/LICENSE.md 5 | */ 6 | 7 | :root { 8 | --accent: #2337ff; 9 | --accent-dark: #000d8a; 10 | --black: 15, 18, 25; 11 | --gray: 96, 115, 159; 12 | --gray-light: 229, 233, 240; 13 | --gray-dark: 34, 41, 57; 14 | --gray-gradient: rgba(var(--gray-light), 50%), #fff; 15 | --box-shadow: 0 2px 6px rgba(var(--gray), 25%), 0 8px 24px rgba(var(--gray), 33%), 16 | 0 16px 32px rgba(var(--gray), 33%); 17 | } 18 | @font-face { 19 | font-family: 'Atkinson'; 20 | src: url('/fonts/atkinson-regular.woff') format('woff'); 21 | font-weight: 400; 22 | font-style: normal; 23 | font-display: swap; 24 | } 25 | @font-face { 26 | font-family: 'Atkinson'; 27 | src: url('/fonts/atkinson-bold.woff') format('woff'); 28 | font-weight: 700; 29 | font-style: normal; 30 | font-display: swap; 31 | } 32 | body { 33 | font-family: 'Atkinson', sans-serif; 34 | margin: 0; 35 | padding: 0; 36 | text-align: left; 37 | background: linear-gradient(var(--gray-gradient)) no-repeat; 38 | background-size: 100% 600px; 39 | word-wrap: break-word; 40 | overflow-wrap: break-word; 41 | color: rgb(var(--gray-dark)); 42 | font-size: 20px; 43 | line-height: 1.7; 44 | } 45 | main { 46 | width: 720px; 47 | max-width: calc(100% - 2em); 48 | margin: auto; 49 | padding: 3em 1em; 50 | } 51 | h1, 52 | h2, 53 | h3, 54 | h4, 55 | h5, 56 | h6 { 57 | margin: 0 0 0.5rem 0; 58 | color: rgb(var(--black)); 59 | line-height: 1.2; 60 | } 61 | h1 { 62 | font-size: 3.052em; 63 | } 64 | h2 { 65 | font-size: 2.441em; 66 | } 67 | h3 { 68 | font-size: 1.953em; 69 | } 70 | h4 { 71 | font-size: 1.563em; 72 | } 73 | h5 { 74 | font-size: 1.25em; 75 | } 76 | strong, 77 | b { 78 | font-weight: 700; 79 | } 80 | a { 81 | color: var(--accent); 82 | } 83 | a:hover { 84 | color: var(--accent); 85 | } 86 | p { 87 | margin-bottom: 1em; 88 | } 89 | .prose p { 90 | margin-bottom: 2em; 91 | } 92 | textarea { 93 | width: 100%; 94 | font-size: 16px; 95 | } 96 | input { 97 | font-size: 16px; 98 | } 99 | table { 100 | width: 100%; 101 | } 102 | img { 103 | max-width: 100%; 104 | height: auto; 105 | border-radius: 8px; 106 | } 107 | code { 108 | padding: 2px 5px; 109 | background-color: rgb(var(--gray-light)); 110 | border-radius: 2px; 111 | } 112 | pre { 113 | padding: 1.5em; 114 | border-radius: 8px; 115 | } 116 | pre > code { 117 | all: unset; 118 | } 119 | blockquote { 120 | border-left: 4px solid var(--accent); 121 | padding: 0 0 0 20px; 122 | margin: 0px; 123 | font-size: 1.333em; 124 | } 125 | hr { 126 | border: none; 127 | border-top: 1px solid rgb(var(--gray-light)); 128 | } 129 | @media (max-width: 720px) { 130 | body { 131 | font-size: 18px; 132 | } 133 | main { 134 | padding: 1em; 135 | } 136 | } 137 | 138 | .sr-only { 139 | border: 0; 140 | padding: 0; 141 | margin: 0; 142 | position: absolute !important; 143 | height: 1px; 144 | width: 1px; 145 | overflow: hidden; 146 | /* IE6, IE7 - a 0 height clip, off to the bottom right of the visible 1px box */ 147 | clip: rect(1px 1px 1px 1px); 148 | /* maybe deprecated but we need to support legacy browsers */ 149 | clip: rect(1px, 1px, 1px, 1px); 150 | /* modern browsers, clip-path works inwards from each corner */ 151 | clip-path: inset(50%); 152 | /* added line to stop words getting smushed together (as they go onto separate lines and some screen readers do not understand line feeds as a space */ 153 | white-space: nowrap; 154 | } 155 | -------------------------------------------------------------------------------- /examples/blog-starter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strictest", 3 | "include": [".astro/types.d.ts", "**/*"], 4 | "exclude": ["dist"] 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dewp/root", 3 | "version": "0.0.0", 4 | "private": true, 5 | "author": "delucis", 6 | "license": "MIT", 7 | "packageManager": "pnpm@9.8.0", 8 | "devDependencies": { 9 | "astro": "^5.0.8" 10 | }, 11 | "dependencies": { 12 | "@changesets/changelog-github": "^0.5.0", 13 | "@changesets/cli": "^2.27.9" 14 | }, 15 | "scripts": { 16 | "version": "pnpm changeset version && pnpm i --no-frozen-lockfile" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/dewp/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # dewp 2 | 3 | ## 0.0.6 4 | 5 | ### Patch Changes 6 | 7 | - [#10](https://github.com/delucis/dewp/pull/10) [`9124366`](https://github.com/delucis/dewp/commit/912436625a292c2ce461264ed98f8aa1de90ec36) Thanks [@igk1972](https://github.com/igk1972)! - Fixes `StatusSchema` for better Wordpress 5.x compatibility 8 | 9 | ## 0.0.5 10 | 11 | ### Patch Changes 12 | 13 | - [`af35d76`](https://github.com/delucis/dewp/commit/af35d76aaff174daff1ebe1a76503e2349ea076e) Thanks [@delucis](https://github.com/delucis)! - Updates description in README & package.json 14 | 15 | ## 0.0.4 16 | 17 | ### Patch Changes 18 | 19 | - [`f5c187b`](https://github.com/delucis/dewp/commit/f5c187b1d37690b88b71d7973895824aaa407147) Thanks [@delucis](https://github.com/delucis)! - Makes `post` field in `comments` collection a reference. 20 | 21 | ## 0.0.3 22 | 23 | ### Patch Changes 24 | 25 | - [`f35d09a`](https://github.com/delucis/dewp/commit/f35d09af571343f9e0066b7310e016e4a7cc8790) Thanks [@delucis](https://github.com/delucis)! - Adds descriptions to schemas and improves some enums and references 26 | 27 | ## 0.0.2 28 | 29 | ### Patch Changes 30 | 31 | - [`ce1bca0`](https://github.com/delucis/dewp/commit/ce1bca07ab401f4215b6bbb7d4c62971c8493136) Thanks [@delucis](https://github.com/delucis)! - Fixes references in `pages` collection schema 32 | 33 | - [`07a9738`](https://github.com/delucis/dewp/commit/07a97385d2b988d760d3a95df281b010e674d661) Thanks [@delucis](https://github.com/delucis)! - Updates docs links 34 | -------------------------------------------------------------------------------- /packages/dewp/README.md: -------------------------------------------------------------------------------- 1 | # DeWP 2 | 3 | Use your WordPress data in Astro projects 4 | 5 | ## Documentation 6 | 7 | [Read the full documentation →](https://delucis.github.io/dewp/) 8 | 9 | ## License 10 | 11 | MIT 12 | -------------------------------------------------------------------------------- /packages/dewp/content-utils.ts: -------------------------------------------------------------------------------- 1 | import { getEntry, type CollectionEntry } from 'astro:content'; 2 | 3 | /** 4 | * Get settings from the `site-settings` content collection. 5 | * Includes `name` (the site title), `description`, and a couple of other handy bits of metadata. 6 | */ 7 | export async function getSiteSettings() { 8 | const entry = await getEntry('site-settings', 'settings'); 9 | return entry.data; 10 | } 11 | 12 | /** 13 | * If pages have parents, WordPress prepends parent slugs to the page slug. 14 | * For example, given a `lion` page with a `big-cats` parent, the page would be served at `/big-cats/lion`. 15 | * 16 | * This function resolves parent pages to construct a multi-segment path like that. 17 | * 18 | * @example 19 | * const lionPage = await getEntry('pages', 'lion'); 20 | * const slug = await resolvePageSlug(lionPage); 21 | * // ^ 'big-cats/lion' 22 | */ 23 | export async function resolvePageSlug(page: CollectionEntry<'pages'>) { 24 | let { parent } = page.data; 25 | const segments = [page.data.slug]; 26 | while (parent) { 27 | const resolvedParent = await getEntry(parent); 28 | parent = resolvedParent.data.parent; 29 | segments.unshift(resolvedParent.data.slug); 30 | } 31 | return segments.join('/'); 32 | } 33 | -------------------------------------------------------------------------------- /packages/dewp/loaders.ts: -------------------------------------------------------------------------------- 1 | import { AstroError } from 'astro/errors'; 2 | import type { DataStore, Loader } from 'astro/loaders'; 3 | import { defineCollection } from 'astro:content'; 4 | import { 5 | CategorySchema, 6 | CommentSchema, 7 | MediaSchema, 8 | PageSchema, 9 | PostSchema, 10 | SiteSettingsSchema, 11 | StatusSchema, 12 | TagSchema, 13 | TaxonomySchema, 14 | TypeSchema, 15 | UserSchema, 16 | } from './schemas.js'; 17 | 18 | type DataEntry = Parameters<DataStore['set']>[0]; 19 | 20 | export function wpCollections({ endpoint }: { endpoint: string }) { 21 | if (!endpoint) { 22 | throw new AstroError( 23 | 'Missing `endpoint` argument.', 24 | 'Please pass a URL to your WordPress REST API endpoint as the `endpoint` option to the WordPress loader. Most commonly this looks something like `https://example.com/wp-json/`' 25 | ); 26 | } 27 | if (!endpoint.endsWith('/')) endpoint += '/'; 28 | const apiBase = new URL(endpoint); 29 | 30 | const l = (type: string) => 31 | makeLoader({ name: `dewp-${type}`, url: new URL(`wp/v2/${type}`, apiBase) }); 32 | 33 | return { 34 | posts: defineCollection({ schema: PostSchema, loader: l('posts') }), 35 | pages: defineCollection({ schema: PageSchema, loader: l('pages') }), 36 | tags: defineCollection({ schema: TagSchema, loader: l('tags') }), 37 | categories: defineCollection({ schema: CategorySchema, loader: l('categories') }), 38 | comments: defineCollection({ schema: CommentSchema, loader: l('comments') }), 39 | users: defineCollection({ schema: UserSchema, loader: l('users') }), 40 | media: defineCollection({ schema: MediaSchema, loader: l('media') }), 41 | statuses: defineCollection({ schema: StatusSchema, loader: l('statuses') }), 42 | taxonomies: defineCollection({ schema: TaxonomySchema, loader: l('taxonomies') }), 43 | types: defineCollection({ schema: TypeSchema, loader: l('types') }), 44 | 'site-settings': defineCollection({ 45 | schema: SiteSettingsSchema, 46 | loader: { 47 | name: 'dewp-site-settings', 48 | async load({ store, parseData }) { 49 | const id = 'settings'; 50 | const rawData = await fetch(apiBase).then((res) => res.json()); 51 | const data = await parseData({ id, data: rawData }); 52 | store.set({ id, data }); 53 | }, 54 | }, 55 | }), 56 | }; 57 | } 58 | 59 | function makeLoader({ name, url }: { name: string; url: URL }) { 60 | const loader: Loader = { 61 | name, 62 | async load({ store, parseData }) { 63 | const items = await fetchAll(url); 64 | for (const rawItem of items) { 65 | const item = await parseData({ id: String(rawItem.id), data: rawItem }); 66 | const storeEntry: DataEntry = { id: String(item.id), data: item }; 67 | if (item.content?.rendered) { 68 | storeEntry.rendered = { html: item.content.rendered }; 69 | } 70 | store.set(storeEntry); 71 | } 72 | }, 73 | }; 74 | return loader; 75 | } 76 | 77 | /** 78 | * Fetch all pages for a paginated WP endpoint. 79 | */ 80 | async function fetchAll(url: URL, page = 1, results: any[] = []) { 81 | url.searchParams.set('per_page', '100'); 82 | url.searchParams.set('page', String(page)); 83 | const response = await fetch(url); 84 | let data = await response.json(); 85 | if (!Array.isArray(data)) { 86 | if (typeof data === 'object') { 87 | data = Object.entries(data).map(([id, val]) => { 88 | if (typeof val === 'object') return { id, ...val }; 89 | return { id }; 90 | }); 91 | } else { 92 | throw new AstroError( 93 | 'Expected WordPress API to return an array of items.', 94 | `Received ${typeof data}:\n\n\`\`\`json\n${JSON.stringify(data, null, 2)}\n\`\`\`` 95 | ); 96 | } 97 | } 98 | results.push(...data); 99 | const totalPages = parseInt(response.headers.get('X-WP-TotalPages') || '1'); 100 | if (page < totalPages) return fetchAll(url, page + 1, results); 101 | return results; 102 | } 103 | -------------------------------------------------------------------------------- /packages/dewp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dewp", 3 | "description": "Use your WordPress data in Astro projects", 4 | "keywords": [ 5 | "withastro", 6 | "astro-loader" 7 | ], 8 | "version": "0.0.6", 9 | "author": "Chris Swithinbank <swithinbank@gmail.com>", 10 | "homepage": "https://delucis.github.io/dewp/", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/delucis/dewp", 14 | "directory": "packages/dewp" 15 | }, 16 | "bugs": "https://github.com/delucis/dewp/issues", 17 | "license": "MIT", 18 | "type": "module", 19 | "exports": { 20 | "./loaders": "./loaders.ts", 21 | "./content-utils": "./content-utils.ts" 22 | }, 23 | "devDependencies": { 24 | "astro": "^5.0.8" 25 | }, 26 | "peerDependencies": { 27 | "astro": "^5.0.8" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/dewp/schemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'astro/zod'; 2 | import { reference } from 'astro:content'; 3 | 4 | /** 5 | * Transform an ID of `0` to `undefined`. 6 | * (The WP API returns an ID of `0` if the relationship doesn’t exist, e.g. for `parent` on top-level items etc.) 7 | */ 8 | function coerceId(id: unknown) { 9 | return id ? String(id) : undefined; 10 | } 11 | 12 | const OpenClosedSchema = z.enum(['open', 'closed', '']); 13 | 14 | export const PostSchema = z.object({ 15 | id: z.number().describe('Unique identifier for the post.'), 16 | date: z.coerce.date().describe("The date the post was published, in the site's timezone."), 17 | date_gmt: z.coerce.date().describe('The date the post was published, as GMT.'), 18 | guid: z.object({ rendered: z.string() }).describe('The globally unique identifier for the post.'), 19 | modified: z.coerce 20 | .date() 21 | .describe("The date the post was last modified, in the site's timezone."), 22 | modified_gmt: z.coerce.date().describe('The date the post was last modified, as GMT.'), 23 | slug: z.string().describe('An alphanumeric identifier for the post unique to its type.'), 24 | status: z.string().describe('A named status for the post.'), 25 | type: z.string().describe('Type of post.'), 26 | link: z.string().url().describe('URL to the post.'), 27 | title: z 28 | .object({ 29 | rendered: z 30 | .string() 31 | .describe( 32 | 'A rendered HTML string for the post title. Best used with Astro’s `set:html` directive.' 33 | ), 34 | }) 35 | .describe('The title for the post.'), 36 | content: z 37 | .object({ 38 | rendered: z.string().describe('A rendered HTML string for the full post content.'), 39 | protected: z.boolean(), 40 | }) 41 | .describe('The content for the post.'), 42 | excerpt: z 43 | .object({ rendered: z.string(), protected: z.boolean() }) 44 | .describe('The excerpt for the post.'), 45 | author: z 46 | .preprocess(coerceId, reference('users')) 47 | .describe('A reference to the author of the post.'), 48 | featured_media: z 49 | .preprocess(coerceId, reference('media').optional()) 50 | .describe('A reference to the featured media for the post.'), 51 | comment_status: OpenClosedSchema.describe('Whether or not comments are open on the post.'), 52 | ping_status: OpenClosedSchema.describe('Whether or not the post can be pinged.'), 53 | sticky: z.boolean().describe('Whether or not the post should be treated as sticky.'), 54 | template: z.string().describe('The theme file to use to display the post.'), 55 | format: z 56 | .enum([ 57 | 'standard', 58 | 'aside', 59 | 'chat', 60 | 'gallery', 61 | 'link', 62 | 'image', 63 | 'quote', 64 | 'status', 65 | 'video', 66 | 'audio', 67 | '', 68 | ]) 69 | .describe('The format for the post.'), 70 | meta: z.array(z.any()).or(z.record(z.any())).describe('Meta fields object or array.'), 71 | categories: z 72 | .preprocess(coerceId, reference('categories')) 73 | .array() 74 | .describe('The terms assigned to the post in the category taxonomy as an array of references.'), 75 | tags: z 76 | .preprocess(coerceId, reference('tags')) 77 | .array() 78 | .describe('The terms assigned to the post in the post_tag taxonomy as an array of references.'), 79 | }); 80 | 81 | export const PageSchema = z.object({ 82 | id: z.number().describe('Unique identifier for the page.'), 83 | date: z.coerce.date().describe("The date the page was published, in the site's timezone."), 84 | date_gmt: z.coerce.date().describe('The date the page was published, as GMT.'), 85 | guid: z.object({ rendered: z.string() }).describe('The globally unique identifier for the page.'), 86 | modified: z.coerce 87 | .date() 88 | .describe("The date the page was last modified, in the site's timezone."), 89 | modified_gmt: z.coerce.date().describe('The date the page was last modified, as GMT.'), 90 | slug: z.string().describe('An alphanumeric identifier for the page unique to its type.'), 91 | status: z 92 | .enum(['publish', 'future', 'draft', 'pending', 'private']) 93 | .describe('A named status for the page.'), 94 | type: z.string().describe('Type of page.'), 95 | link: z.string().describe('URL to the page.'), 96 | title: z 97 | .object({ 98 | rendered: z 99 | .string() 100 | .describe( 101 | 'A rendered HTML string for the page title. Best used with Astro’s `set:html` directive.' 102 | ), 103 | }) 104 | .describe('The title for the page.'), 105 | content: z 106 | .object({ 107 | rendered: z.string().describe('A rendered HTML string for the full page content.'), 108 | protected: z.boolean(), 109 | }) 110 | .describe('The content for the page.'), 111 | excerpt: z 112 | .object({ 113 | rendered: z.string().describe('A rendered HTML string for the page excerpt.'), 114 | protected: z.boolean(), 115 | }) 116 | .describe('The excerpt for the page.'), 117 | author: z 118 | .preprocess(coerceId, reference('users')) 119 | .describe('A reference to the author of the page.'), 120 | featured_media: z 121 | .preprocess(coerceId, reference('media').optional()) 122 | .describe('A reference to the featured media for the post.'), 123 | parent: z 124 | .preprocess(coerceId, reference('pages').optional()) 125 | .describe('A reference to a parent page.'), 126 | menu_order: z.number().describe('The order of the page in relation to other pages.'), 127 | comment_status: OpenClosedSchema.describe('Whether or not comments are open on this page.'), 128 | ping_status: OpenClosedSchema.describe('Whether or not the page can be pinged.'), 129 | template: z.string().describe('The theme file to use to display the page.'), 130 | meta: z.array(z.any()).or(z.record(z.any())).describe('Meta fields.'), 131 | }); 132 | 133 | export const TagSchema = z.object({ 134 | id: z.number().describe('Unique identifier for the term.'), 135 | count: z.number().describe('Number of published posts for the term.'), 136 | description: z.string().describe('HTML description of the term.'), 137 | link: z.string().url().describe('URL of the term.'), 138 | name: z.string().describe('HTML title for the term.'), 139 | slug: z.string().describe('An alphanumeric identifier for the term unique to its type.'), 140 | taxonomy: z.string().describe('Type attribution for the term.'), 141 | meta: z.array(z.any()).or(z.record(z.any())).describe('Meta fields.'), 142 | }); 143 | 144 | export const CategorySchema = TagSchema.extend({ 145 | parent: z 146 | .preprocess(coerceId, reference('categories').optional()) 147 | .describe('A reference to a parent category.'), 148 | }); 149 | 150 | export const CommentSchema = z.object({ 151 | id: z.number().describe('Unique identifier for the comment.'), 152 | author: z 153 | .preprocess(coerceId, reference('users').optional()) 154 | .describe('A reference to the author of the comment, if author was a user.'), 155 | author_name: z.string().describe('Display name for the comment author.'), 156 | author_url: z.string().describe('URL for the comment author.'), 157 | content: z.object({ rendered: z.string() }).describe('The content for the comment.'), 158 | date: z.coerce.date().describe("The date the comment was published, in the site's timezone."), 159 | date_gmt: z.coerce.date().describe('The date the comment was published, as GMT.'), 160 | link: z.string().url().describe('URL to the comment.'), 161 | parent: z 162 | .preprocess(coerceId, reference('comments').optional()) 163 | .describe('A reference to a parent comment.'), 164 | post: z 165 | .preprocess(coerceId, z.union([reference('posts'), reference('pages')]).optional()) 166 | .describe('A reference to the associated post.'), 167 | status: z.string().describe('State of the comment.'), 168 | type: z.string().describe('Type of the comment.'), 169 | author_avatar_urls: z.record(z.string().url()).describe('Avatar URLs for the comment author.'), 170 | meta: z.array(z.any()).or(z.record(z.any())).describe('Meta fields.'), 171 | }); 172 | 173 | export const UserSchema = z.object({ 174 | id: z.number().describe('Unique identifier for the user.'), 175 | name: z.string().describe('Display name for the user.'), 176 | url: z.string().describe('URL of the user.'), 177 | description: z.string().describe('Description of the user.'), 178 | link: z.string().url().describe('Author URL of the user.'), 179 | slug: z.string().describe('An alphanumeric identifier for the user.'), 180 | avatar_urls: z.record(z.string().url()).describe('Avatar URLs for the user.'), 181 | meta: z.array(z.any()).or(z.record(z.any())).describe('Meta fields.'), 182 | }); 183 | 184 | export const MediaSchema = z.object({ 185 | id: z.number().describe('Unique identifier for the item.'), 186 | date: z.coerce 187 | .date() 188 | .nullable() 189 | .describe("The date the item was published, in the site's timezone."), 190 | date_gmt: z.coerce.date().nullable().describe('The date the post was published, as GMT.'), 191 | guid: z.object({ rendered: z.string() }).describe('The globally unique identifier for the item.'), 192 | link: z.string().url().describe('URL to the media item.'), 193 | modified: z.coerce 194 | .date() 195 | .describe("The date the item was last modified, in the site's timezone."), 196 | modified_gmt: z.coerce.date().describe('The date the item was last modified, as GMT.'), 197 | slug: z.string().describe('An alphanumeric identifier for the item unique to its type.'), 198 | status: z 199 | .enum(['publish', 'future', 'draft', 'pending', 'private', 'inherit']) 200 | .describe('A named status for the item.'), 201 | type: z.string().describe('Type of item.'), 202 | title: z.object({ rendered: z.string() }).describe('The title for the post.'), 203 | author: z 204 | .preprocess(coerceId, reference('users')) 205 | .describe('A reference to the author of the post.'), 206 | comment_status: OpenClosedSchema.describe('Whether or not comments are open on the post.'), 207 | ping_status: OpenClosedSchema.describe('Whether or not the post can be pinged.'), 208 | meta: z.array(z.any()).or(z.record(z.any())).describe('Meta fields.'), 209 | template: z.string().describe('The theme file to use to display the post.'), 210 | alt_text: z.string().describe('Alternative text to display when attachment is not displayed.'), 211 | caption: z.object({ rendered: z.string() }).describe('The attachment caption.'), 212 | description: z.object({ rendered: z.string() }).describe('The attachment description.'), 213 | media_type: z.enum(['image', 'file']).describe('Attachment type.'), 214 | mime_type: z.string().describe('The attachment MIME type.'), 215 | media_details: z 216 | .object({ 217 | filesize: z.number(), 218 | sizes: z.record( 219 | z.object({ 220 | width: z.number(), 221 | height: z.number(), 222 | file: z.string(), 223 | filesize: z.number().optional(), 224 | mime_type: z.string(), 225 | source_url: z.string(), 226 | }) 227 | ), 228 | }) 229 | .and( 230 | z.union([ 231 | z.object({ 232 | width: z.number(), 233 | height: z.number(), 234 | file: z.string(), 235 | image_meta: z.record(z.any()), 236 | }), 237 | z.object({ 238 | dataformat: z.string(), 239 | channels: z.number(), 240 | sample_rate: z.number(), 241 | bitrate: z.number(), 242 | channelmode: z.string(), 243 | bitrate_mode: z.string(), 244 | lossless: z.boolean(), 245 | encoder_options: z.string(), 246 | compression_ratio: z.number(), 247 | fileformat: z.string(), 248 | mime_type: z.string(), 249 | length: z.number(), 250 | length_formatted: z.string(), 251 | encoded_by: z.string(), 252 | title: z.string(), 253 | encoder_settings: z.string(), 254 | artist: z.string(), 255 | album: z.string(), 256 | }), 257 | z.object({}), 258 | ]) 259 | ) 260 | .or(z.object({})) 261 | .describe('An object containing details about the media file, specific to its type.'), 262 | post: z 263 | .preprocess(coerceId, reference('posts').optional()) 264 | .describe('The ID for the associated post of the attachment.'), 265 | source_url: z.string().describe('URL to the original attachment file.'), 266 | }); 267 | 268 | export const StatusSchema = z.object({ 269 | id: z.string().describe('Unique identifier for this status definition.'), 270 | name: z.string().describe('The title for the status.'), 271 | public: z 272 | .boolean() 273 | .describe('Whether posts of this status should be shown in the front end of the site.'), 274 | queryable: z.boolean().describe('Whether posts with this status should be publicly-queryable.'), 275 | slug: z.string().describe('An alphanumeric identifier for the status.'), 276 | date_floating: z 277 | .boolean() 278 | .optional() 279 | .describe('Whether posts of this status may have floating published dates.'), 280 | }); 281 | 282 | export const TaxonomySchema = z.object({ 283 | id: z.string().describe('Unique identifier for this taxonomy definition.'), 284 | description: z.string().describe('A human-readable description of the taxonomy.'), 285 | hierarchical: z.boolean().describe('Whether or not the taxonomy should have children.'), 286 | name: z.string().describe('The title for the taxonomy.'), 287 | slug: z.string().describe('An alphanumeric identifier for the taxonomy.'), 288 | types: z.string().array().describe('Types associated with the taxonomy.'), 289 | rest_base: z.string().describe('REST base route for the taxonomy.'), 290 | rest_namespace: z.string().default('wp/v2').describe('REST namespace route for the taxonomy.'), 291 | }); 292 | 293 | export const TypeSchema = z.object({ 294 | id: z.string().describe('A unique identifier for the post type.'), 295 | description: z.string().describe('A human-readable description of the post type.'), 296 | hierarchical: z.boolean().describe('Whether or not the post type should have children.'), 297 | name: z.string().describe('The title for the post type.'), 298 | slug: z.string().describe('An alphanumeric identifier for the post type.'), 299 | has_archive: z 300 | .union([z.boolean(), z.string()]) 301 | .default(false) 302 | .describe( 303 | 'If the value is a string, the value will be used as the archive slug. If the value is false the post type has no archive.' 304 | ), 305 | taxonomies: z.string().array(), 306 | rest_base: z.string().describe('REST base route for the post type.'), 307 | rest_namespace: z.string().default('wp/v2').describe("REST route's namespace for the post type."), 308 | icon: z.string().nullable().default(null).describe('The icon for the post type.'), 309 | }); 310 | 311 | export const SiteSettingsSchema = z.object({ 312 | id: z.literal('settings').default('settings'), 313 | name: z.string().describe('The site title.'), 314 | description: z.string().describe('A human-readable description of the site.'), 315 | url: z.string().describe('The URL of the site.'), 316 | home: z.string().describe('The URL of the site homepage. (Usually the same as `url`.)'), 317 | gmt_offset: z.coerce 318 | .number() 319 | .describe("The site's timezone expressed as an offset in hours from GMT"), 320 | timezone_string: z.string().describe('The site\'s timezone as a string, e.g. `"Europe/Paris"`.'), 321 | site_logo: z 322 | .preprocess(coerceId, reference('media').optional()) 323 | .describe('Reference to a media attachment to use as the site logo.'), 324 | site_icon: z 325 | .preprocess(coerceId, reference('media').optional()) 326 | .describe('Reference to a media attachment to use as the site icon.'), 327 | site_icon_url: z.string().optional().describe('URL to a resource to use as the site icon.'), 328 | }); 329 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/**/*' 3 | - 'examples/**/*' 4 | - 'docs' 5 | 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strictest" 3 | } --------------------------------------------------------------------------------