├── .editorconfig ├── .gitignore ├── README.md ├── demo ├── astro.config.js ├── package.json ├── public │ └── favicon.ico └── src │ ├── layouts │ ├── Contents.global.d.ts │ ├── Contents.ts │ ├── Document.astro │ └── Document.css │ └── pages │ └── index.astro ├── package.json ├── packages └── contentful │ ├── README.md │ ├── astro-contentful.d.ts │ ├── astro-contentful.js │ ├── bundled │ ├── contentful.js │ └── dotenv.js │ ├── global.d.ts │ ├── integrations │ ├── contentful-astro-integration.d.ts │ ├── contentful-astro-integration.js │ ├── contentful-middleware.html │ ├── contentful-middleware.js │ ├── contentful-response.js │ ├── contentful-vite-plugin.js │ ├── createMeta.d.ts │ └── createMeta.js │ └── package.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = tab 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.astro] 12 | insert_final_newline = false 13 | 14 | [*.md] 15 | indent_size = 2 16 | indent_style = space 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | package-lock.json 4 | *.env* 5 | *.log* 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Astro Contentful 2 | 3 | **Astro Contentful** lets you use **Contentful** in [Astro](https://astro.build). 4 | 5 | [![NPM Version][npm-img]][npm-url] 6 | [![Open in StackBlitz][stackblitz-img]][stackblitz-url] 7 | 8 | ```astro 9 | --- 10 | import Entry from '@astropub/contentful:entries/t1h1x38!astro' 11 | --- 12 |

13 | 14 |

15 | 16 | 17 |

Fallback content.

18 |
19 | ``` 20 | 21 | ```astro 22 | --- 23 | import EntryList from '@astropub/contentful:entries?content_type=blog!astro' 24 | --- 25 | { 26 | Entry => ( 27 |

28 | 29 |

30 | 31 | 32 |

Fallback content.

33 |
34 | ) 35 | }
36 | ``` 37 | 38 | ## Usage 39 | 40 | Install **Astro Contentful** to your project. 41 | 42 | ```shell 43 | npm install @astropub/contentful 44 | ``` 45 | 46 | Use **Astro Contentful** components in your project. 47 | 48 | ```astro 49 | --- 50 | // data 51 | import entry from '@astropub/contentful:entries/t1h1x38' 52 | import entryList from '@astropub/contentful:entries' 53 | import oneEntryList from '@astropub/contentful:entries?content_type=blog' 54 | 55 | // components 56 | import Entry from '@astropub/contentful:entries/t1h1x38!astro' 57 | import EntryList from '@astropub/contentful:entries!astro' 58 | import OneEntryList from '@astropub/contentful:entries?content_type=blog!astro' 59 | --- 60 | ``` 61 | 62 | Enjoy! 63 | 64 | ## Project Structure 65 | 66 | Inside of this Astro project, you'll see the following folders and files: 67 | 68 | ``` 69 | / 70 | ├── demo/ 71 | │ ├── public/ 72 | │ └── src/ 73 | │ └── pages/ 74 | ├── index.astro 75 | │ └── ...etc 76 | └── packages/ 77 | └── contentful/ 78 | ├── package.json 79 | └── ...etc 80 | ``` 81 | 82 | This project uses **workspaces** to develop a single package, `@astropub/contentful`. 83 | 84 | It also includes a minimal Astro project, `demo`, for developing and demonstrating the component. 85 | 86 | ## Commands 87 | 88 | All commands are run from the root of the project, from a terminal: 89 | 90 | | Command | Action | 91 | |:----------------|:---------------------------------------------| 92 | | `npm install` | Installs dependencies | 93 | | `npm run start` | Starts local dev server at `localhost:3000` | 94 | | `npm run build` | Build your production site to `./dist/` | 95 | | `npm run serve` | Preview your build locally, before deploying | 96 | 97 | Want to learn more? 98 | Read the [Astro documentation][docs-url] or jump into the [Astro Discord][chat-url]. 99 | 100 | [chat-url]: https://astro.build/chat 101 | [docs-url]: https://github.com/withastro/astro 102 | 103 | [npm-img]: https://img.shields.io/npm/v/@astropub/contentful?color=%23444&label=&labelColor=%23CB0000&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjE1MCAxNTAgNDAwIDQwMCIgZmlsbD0iI0ZGRiI+PHBhdGggZD0iTTE1MCA1NTBoMjAwVjI1MGgxMDB2MzAwaDEwMFYxNTBIMTUweiIvPjwvc3ZnPg==&style=for-the-badge 104 | [npm-url]: https://www.npmjs.com/package/@astropub/contentful 105 | [stackblitz-img]: https://img.shields.io/badge/-Open%20in%20Stackblitz-%231374EF?color=%23444&labelColor=%231374EF&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjEwIDggMTIgMTgiIGhlaWdodD0iMTgiIGZpbGw9IiNGRkYiPjxwYXRoIGQ9Ik0xMCAxNy42aDUuMmwtMyA3LjRMMjIgMTQuNGgtNS4ybDMtNy40TDEwIDE3LjZaIi8+PC9zdmc+&style=for-the-badge 106 | [stackblitz-url]: https://stackblitz.com/github/astro-community/contentful 107 | -------------------------------------------------------------------------------- /demo/astro.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { defineConfig } from 'astro/config' 3 | import contentfulIntegration from '@astropub/contentful' 4 | 5 | export default defineConfig({ 6 | integrations: [ 7 | contentfulIntegration() 8 | ], 9 | server: { 10 | host: true, 11 | }, 12 | vite: { 13 | ssr: { 14 | noExternal: '@astropub/contentful' 15 | } 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astro-contentful-demo", 3 | "type": "module", 4 | "version": "0.1.0", 5 | "private": true, 6 | "scripts": { 7 | "start": "astro dev", 8 | "build": "astro build", 9 | "serve": "astro preview" 10 | }, 11 | "devDependencies": { 12 | "@astropub/contentful": "0.1.0", 13 | "astro": "latest" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astro-community/contentful/92085d85246a77bb3ac0f1c6c5d55b7710c7ee2e/demo/public/favicon.ico -------------------------------------------------------------------------------- /demo/src/layouts/Contents.global.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'astro/server/render/index.js' { 2 | const exports: any 3 | 4 | export function renderTemplate(htmlParts: TemplateStringsArray, ...expressions: any[]): Promise 5 | 6 | export function renderSlot(result: any, slotted: string, fallback?: any): Promise 7 | } 8 | -------------------------------------------------------------------------------- /demo/src/layouts/Contents.ts: -------------------------------------------------------------------------------- 1 | import { renderTemplate, renderSlot } from 'astro/server/render/index.js' 2 | 3 | export const Contents = async (result: any, props: any, slots: any): Promise => { 4 | return renderTemplate`${ 5 | props.of 6 | ? new String(props.of) 7 | : renderSlot(result, slots.default) 8 | }`; 9 | } 10 | 11 | Contents.isAstroComponentFactory = true 12 | 13 | export default new Proxy(Contents, { 14 | get(target, name) { 15 | // pass through any existing properties or asynchronous `.then` check 16 | if (name in target || name === 'then') return target[name] 17 | 18 | return Object.assign( 19 | async (result: any, props: any, slots: any) => await target( 20 | result, 21 | { 22 | // pre-define the `of` attribute, still allowing overrides 23 | 'of': name, 24 | ...props, 25 | }, 26 | slots 27 | ), 28 | // identify this function as an astro component 29 | { 30 | isAstroComponentFactory: true, 31 | } 32 | ) 33 | }, 34 | }) 35 | -------------------------------------------------------------------------------- /demo/src/layouts/Document.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import "./Document.css" 3 | --- 4 | 5 | 6 | 7 | 8 | {Astro.props.title} 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /demo/src/layouts/Document.css: -------------------------------------------------------------------------------- 1 | :root { 2 | tab-size: 4; 3 | } 4 | 5 | pre { 6 | line-height: 1.5; 7 | } 8 | -------------------------------------------------------------------------------- /demo/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Document from '../layouts/Document.astro' 3 | 4 | const { entryList, EntryList } = await import('@astropub/contentful:entries?content_type=communityLink!astro') 5 | --- 6 | 7 | { 8 | Entry => ( 9 |

10 | Fallback Title 11 |

12 | 13 | Fallback content. 14 | ) 15 | }
16 | 17 |

18 | Hey, there was a problem. 19 |

20 |

21 | There were no entries or the `.env.local` file is not setup. 22 |

23 |
24 |
-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astro-contentful-root", 3 | "type": "module", 4 | "version": "0.1.0", 5 | "workspaces": [ 6 | "demo", 7 | "packages/*" 8 | ], 9 | "scripts": { 10 | "bump:patch": "npm --workspaces --git-tag-version false version patch && npm --git-tag-version false version patch", 11 | "bump:minor": "npm --workspaces --git-tag-version false version minor && npm --git-tag-version false version minor", 12 | "bump:major": "npm --workspaces --git-tag-version false version major && npm --git-tag-version false version major", 13 | "start": "cd demo; astro dev", 14 | "build": "cd demo; astro build", 15 | "serve": "cd demo; astro preview", 16 | "release": "npm --workspaces publish --access public" 17 | }, 18 | "prettier": { 19 | "semi": false, 20 | "singleQuote": true, 21 | "trailingComma": "es5", 22 | "useTabs": true 23 | }, 24 | "stackblitz": { 25 | "startCommand": "npm start", 26 | "env": { 27 | "ENABLE_CJS_IMPORTS": true 28 | } 29 | }, 30 | "devDependencies": { 31 | "astro": "latest", 32 | "types-object": "0.3.0" 33 | }, 34 | "private": true 35 | } 36 | -------------------------------------------------------------------------------- /packages/contentful/README.md: -------------------------------------------------------------------------------- 1 | # Astro Contentful 2 | 3 | **Astro Contentful** lets you use **Contentful** in [Astro](https://astro.build). 4 | 5 | [![NPM Version][npm-img]][npm-url] 6 | [![Open in StackBlitz][stackblitz-img]][stackblitz-url] 7 | 8 | ```astro 9 | --- 10 | import Entry from '@astropub/contentful:entries/t1h1x38!astro' 11 | --- 12 |

13 | 14 |

15 | 16 | 17 |

Fallback content.

18 |
19 | ``` 20 | 21 | ```astro 22 | --- 23 | import EntryList from '@astropub/contentful:entries?content_type=blog!astro' 24 | --- 25 | { 26 | Entry => ( 27 |

28 | 29 |

30 | 31 | 32 |

Fallback content.

33 |
34 | ) 35 | }
36 | ``` 37 | 38 | ## Usage 39 | 40 | Install **Astro Contentful** to your project. 41 | 42 | ```shell 43 | npm install @astropub/contentful 44 | ``` 45 | 46 | Use **Astro Contentful** components in your project. 47 | 48 | ```astro 49 | --- 50 | // data 51 | import entry from '@astropub/contentful:entries/t1h1x38' 52 | import entryList from '@astropub/contentful:entries' 53 | import oneEntryList from '@astropub/contentful:entries?content_type=blog' 54 | 55 | // components 56 | import Entry from '@astropub/contentful:entries/t1h1x38!astro' 57 | import EntryList from '@astropub/contentful:entries!astro' 58 | import oneEntryList from '@astropub/contentful:entries?content_type=blog!astro' 59 | --- 60 | ``` 61 | 62 | Enjoy! 63 | 64 | [npm-img]: https://img.shields.io/npm/v/@astropub/contentful?color=%23444&label=&labelColor=%23CB0000&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjE1MCAxNTAgNDAwIDQwMCIgZmlsbD0iI0ZGRiI+PHBhdGggZD0iTTE1MCA1NTBoMjAwVjI1MGgxMDB2MzAwaDEwMFYxNTBIMTUweiIvPjwvc3ZnPg==&style=for-the-badge 65 | [npm-url]: https://www.npmjs.com/package/@astropub/contentful 66 | [stackblitz-img]: https://img.shields.io/badge/-Open%20in%20Stackblitz-%231374EF?color=%23444&labelColor=%231374EF&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjEwIDggMTIgMTgiIGhlaWdodD0iMTgiIGZpbGw9IiNGRkYiPjxwYXRoIGQ9Ik0xMCAxNy42aDUuMmwtMyA3LjRMMjIgMTQuNGgtNS4ybDMtNy40TDEwIDE3LjZaIi8+PC9zdmc+&style=for-the-badge 67 | [stackblitz-url]: https://stackblitz.com/github/astro-community/contentful 68 | -------------------------------------------------------------------------------- /packages/contentful/astro-contentful.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | export { 4 | contentfulIntegration as default 5 | } from './integrations/contentful-astro-integration.d' 6 | 7 | export function htmltorichtext(value: any): any 8 | export function richtexttohtml(value: any): any 9 | 10 | // export function Entry(Props: Record): any 11 | -------------------------------------------------------------------------------- /packages/contentful/astro-contentful.js: -------------------------------------------------------------------------------- 1 | export { contentfulIntegration as default } from './integrations/contentful-astro-integration.js' 2 | 3 | export * from './bundled/contentful.js' 4 | -------------------------------------------------------------------------------- /packages/contentful/bundled/dotenv.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Scott Motte 2 | // All rights reserved. 3 | 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are met: 6 | 7 | // * Redistributions of source code must retain the above copyright notice, this 8 | // list of conditions and the following disclaimer. 9 | 10 | // * Redistributions in binary form must reproduce the above copyright notice, 11 | // this list of conditions and the following disclaimer in the documentation 12 | // and/or other materials provided with the distribution. 13 | 14 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg 26 | 27 | // Parser src into an Object 28 | export function parseEnv (src) { 29 | const obj = {} 30 | 31 | // Convert buffer to string 32 | let lines = src.toString() 33 | 34 | // Convert line breaks to same format 35 | lines = lines.replace(/\r\n?/mg, '\n') 36 | 37 | let match 38 | while ((match = LINE.exec(lines)) != null) { 39 | const key = match[1] 40 | 41 | // Default undefined or null to empty string 42 | let value = (match[2] || '') 43 | 44 | // Remove whitespace 45 | value = value.trim() 46 | 47 | // Check if double quoted 48 | const maybeQuote = value[0] 49 | 50 | // Remove surrounding quotes 51 | value = value.replace(/^(['"`])([\s\S]*)\1$/mg, '$2') 52 | 53 | // Expand newlines if double quoted 54 | if (maybeQuote === '"') { 55 | value = value.replace(/\\n/g, '\n') 56 | value = value.replace(/\\r/g, '\r') 57 | } 58 | 59 | // Add to object 60 | obj[key] = value 61 | } 62 | 63 | return obj 64 | } 65 | -------------------------------------------------------------------------------- /packages/contentful/global.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Contentful { 2 | interface Locales { 3 | sys: object, 4 | total: number, 5 | skip: number, 6 | limit: number, 7 | items: { 8 | name: string, 9 | internal_code: string, 10 | code: string, 11 | fallbackCode: unknown, 12 | default: boolean, 13 | contentManagementApi: boolean, 14 | contentDeliveryApi: boolean, 15 | optional: boolean, 16 | sys: object[] 17 | }[] 18 | } 19 | 20 | interface Sys { 21 | type: string 22 | linkType: string 23 | id: string 24 | } 25 | 26 | interface PlainTextField { 27 | [key: string]: string 28 | } 29 | 30 | interface RichTextField { 31 | nodeType: string 32 | data: {} 33 | content: { 34 | nodeType: string 35 | data: {} 36 | value: string 37 | marks: { 38 | type: string 39 | }[] 40 | }[] 41 | } 42 | 43 | interface Entry { 44 | metadata: { 45 | tags: [] 46 | } 47 | sys: { 48 | space: { 49 | sys: Sys 50 | } 51 | id: string 52 | type: string 53 | createdAt: string 54 | updatedAt: string 55 | environment: { 56 | sys: Sys 57 | } 58 | publishedVersion: number 59 | publishedAt: string 60 | firstPublishedAt: string 61 | createdBy: { 62 | sys:Sys 63 | } 64 | updatedBy: { 65 | sys: Sys 66 | } 67 | publishedCounter: number 68 | version: number 69 | publishedBy: { 70 | sys: Sys 71 | } 72 | contentType: { 73 | sys: Sys 74 | } 75 | } 76 | fields: { 77 | [key: string]: PlainTextField | RichTextField 78 | } 79 | } 80 | 81 | interface EntryList { 82 | sys: { 83 | type: 'Array' 84 | } 85 | total: number 86 | skip: number 87 | limit: number 88 | items: Entry[] 89 | } 90 | 91 | interface EntryProps { 92 | of: string 93 | } 94 | 95 | interface EntryComponent { 96 | (props: EntryProps): any 97 | 98 | of(name: string): string | null 99 | } 100 | 101 | interface EntryListComponent { 102 |

(props: EntryListProp

): any 103 | 104 | None(props: Record): any 105 | } 106 | 107 | type EntryListProp> = { 108 | [K in keyof T]: K extends 'children' 109 | ? { 110 | (entry: any): any 111 | } 112 | : T[K] 113 | } & { 114 | children?: { 115 | (item: typeof EntryComponent): any 116 | } 117 | } 118 | 119 | interface Fields { 120 | [name: string]: string | null 121 | } 122 | 123 | var EntryList: EntryList 124 | var Entry: Entry 125 | var EntryComponent: EntryComponent 126 | var EntryListComponent: EntryListComponent 127 | var Fields: Fields 128 | var Locales: Locales 129 | } 130 | 131 | // Entry 132 | // ----------------------------------------------------------------------------- 133 | 134 | declare module '@astropub/contentful:entries/*!astro' { 135 | export type Props = Contentful.EntryProps 136 | 137 | export var Entry: Contentful.EntryComponent 138 | export var entry: Contentful.Entry 139 | 140 | export default Contentful.EntryComponent 141 | } 142 | 143 | declare module '@astropub/contentful:entries/*' { 144 | export type Props = Contentful.EntryProps 145 | 146 | export var Entry: Contentful.EntryComponent 147 | export var entry: Contentful.Entry 148 | 149 | export default Contentful.Entry 150 | } 151 | 152 | // EntryList 153 | // ----------------------------------------------------------------------------- 154 | 155 | declare module '@astropub/contentful:entries*!astro' { 156 | export type Props = Contentful.EntryProps 157 | 158 | export var EntryList: Contentful.EntryListComponent 159 | export var entryList: Contentful.Entry 160 | 161 | export default Contentful.EntryListComponent 162 | } 163 | 164 | declare module '@astropub/contentful:entries*' { 165 | export type Props = Contentful.EntryProps 166 | 167 | export var EntryList: Contentful.EntryListComponent 168 | export var entryList: Contentful.EntryList 169 | 170 | export default Contentful.EntryList 171 | } 172 | 173 | // ----------------------------------------------------------------------------- 174 | 175 | declare module '@astropub/contentful:locales*' { 176 | export var locales: Contentful.Locales 177 | 178 | export default Contentful.Locales 179 | } 180 | 181 | declare module '@astropub/contentful:content_types/*' { 182 | const exports: any 183 | 184 | export default exports 185 | } 186 | -------------------------------------------------------------------------------- /packages/contentful/integrations/contentful-astro-integration.d.ts: -------------------------------------------------------------------------------- 1 | import { AstroIntegration } from 'astro' 2 | 3 | export const contentfulIntegration: () => AstroIntegration 4 | 5 | /** Current working directory of the Node.js Process. ([reference](https://nodejs.org/api/process.html#processcwd)) */ 6 | export type CurrentWorkingDirectory = string 7 | 8 | /** Environment variables. ([reference](https://en.wikipedia.org/wiki/Environment_variable)) */ 9 | export interface EnvironmentVariables { 10 | [variable: string]: string 11 | } 12 | 13 | /** Mode the app is running in. ([reference](https://vitejs.dev/guide/env-and-mode.html#modes)) */ 14 | export type Mode = string 15 | 16 | export interface Internals { 17 | /** Current working directory of the Node.js Process. ([reference](https://nodejs.org/api/process.html#processcwd)) */ 18 | cwd: CurrentWorkingDirectory 19 | 20 | /** Environment variables. ([reference](https://en.wikipedia.org/wiki/Environment_variable)) */ 21 | env: EnvironmentVariables 22 | 23 | /** Mode the app is running in. ([reference](https://vitejs.dev/guide/env-and-mode.html#modes)) */ 24 | mode: Mode 25 | } 26 | -------------------------------------------------------------------------------- /packages/contentful/integrations/contentful-astro-integration.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { createMeta } from './createMeta.js' 4 | import { contentfulMiddleware } from './contentful-middleware.js' 5 | import { contentfulVite } from './contentful-vite-plugin.js' 6 | 7 | export const contentfulIntegration = (/** @type {AstroIntegrationOpts} */ opts = any) => { 8 | const meta = createMeta() 9 | 10 | /** @type {AstroIntegration} */ 11 | const integration = { 12 | name: 'astro:contentful', 13 | hooks: { 14 | 'astro:config:setup'({ config, updateConfig }) { 15 | meta.update(config.root) 16 | 17 | updateConfig({ 18 | vite: { 19 | plugins: [ 20 | contentfulVite({ meta }) 21 | ], 22 | } 23 | }) 24 | }, 25 | 'astro:config:done'({ config }) { 26 | meta.protocol = config.vite?.server?.https ? 'https:' : 'http:' 27 | 28 | meta.hostname = ( 29 | typeof config.server.host === 'string' 30 | ? config.server.host 31 | : 'localhost' 32 | ) 33 | 34 | meta.port = String(config.server.port || meta.port) 35 | }, 36 | 'astro:server:setup'({ server }) { 37 | if (!meta.env.CONTENTFUL_ACCESS_TOKEN) { 38 | server.middlewares.use(contentfulMiddleware({ meta })) 39 | } 40 | }, 41 | async 'astro:server:start'(options) { 42 | meta.port = String(options.address.port || meta.port) 43 | }, 44 | }, 45 | } 46 | 47 | return integration 48 | } 49 | 50 | const any = /** @type {any} */ (null) 51 | 52 | /** @typedef {ReturnType} Meta */ 53 | /** @typedef {import('astro').AstroIntegration} AstroIntegration */ 54 | /** @typedef {Partial} AstroIntegrationOpts */ 55 | /** @typedef {import('vite').Plugin} VitePlugin */ 56 | /** @typedef {import('./contentful-vite-plugin').ContentfulConfig} ContentfulConfig */ 57 | -------------------------------------------------------------------------------- /packages/contentful/integrations/contentful-middleware.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Astro Contentful 5 | 6 | 187 | 188 |

189 |

Astro Contentful

190 |

191 | Thanks for adding Contentful integration! 192 |

193 |

194 | Now it’s time to add your Access Token: 195 |

196 | 199 |

200 | 201 |

202 |
203 |

204 | Having trouble? 205 |

206 |

207 | Visit your Contentful App. 208 |

209 |

210 | From the menu, 211 | select SettingsAPI keys. 212 |

213 |

214 | From the API page, 215 | retreive a key from the Content Delivery or Content Management sections. 216 |

217 |
218 | 307 | 308 | -------------------------------------------------------------------------------- /packages/contentful/integrations/contentful-middleware.js: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs' 2 | import { parseEnv } from '../bundled/dotenv.js' 3 | 4 | export const contentfulMiddleware = (/** @type {{ meta: Meta }} */ { meta }) => { 5 | const middleware = /** @type {ContentfulMiddleware} */ ((req, res, next) => { 6 | const url = new URL(req.url, meta.origin) 7 | 8 | if (url.pathname === middlewareConfig.pathToHTML) { 9 | res.setHeader('Content-Type', 'text/html;charset=utf-8') 10 | 11 | return res.end( 12 | fs.readFileSync(pathToHtml, 'utf-8').replace(/\{\{([\w_]+)\}\}/g, ($0, $1) => { 13 | return ( 14 | $1 in meta.env 15 | ? meta.env[$1] 16 | : $1 in middlewareConfig 17 | ? middlewareConfig[$1] 18 | : '' 19 | ) 20 | }) 21 | ) 22 | } 23 | 24 | if (url.pathname === middlewareConfig.pathToJSON) { 25 | if (req.method == 'POST') { 26 | let body = ''; 27 | 28 | req.on('data', function (data) { 29 | body += data 30 | }) 31 | 32 | req.on('end', async () => { 33 | res.setHeader('Content-Type', 'application/json;charset=utf-8') 34 | 35 | try { 36 | const update = JSON.parse(body) 37 | 38 | if (update.token && update.space) { 39 | const checkURL = new URL(`https://api.contentful.com/spaces/${update.space}`) 40 | const checkReq = new Request(checkURL, { 41 | cache: 'no-cache', 42 | headers: { 43 | 'Accept': 'application/json', 44 | 'Authorization': 'Bearer ' + update.token, 45 | 'Accept-Encoding': 'gzip', 46 | 'User-Agent': 'node.js/', 47 | 'x-contentful-user-agent': 'sdk contentful-management.js/10.12.0;', 48 | }, 49 | }) 50 | 51 | res.setHeader('Content-Type', 'application/json;charset=utf-8') 52 | 53 | const checkRes = await fetch(checkReq) 54 | const checkJSO = await checkRes.json() 55 | 56 | if (checkJSO?.sys?.type === 'Space') { 57 | const envValu = { 58 | CONTENTFUL_SPACE: update.space, 59 | CONTENTFUL_ACCESS_TOKEN: update.token, 60 | CONTENTFUL_ENVIRONMENT: 'master', 61 | CONTENTFUL_LOCALE: 'en-US', 62 | } 63 | const envPath = new URL('.env.local', meta.cwd) 64 | 65 | touchFileSync(envPath) 66 | 67 | const envOpts = parseEnv(fs.readFileSync(envPath)) 68 | 69 | envOpts.CONTENTFUL_SPACE = update.space 70 | envOpts.CONTENTFUL_ACCESS_TOKEN = update.token 71 | envOpts.CONTENTFUL_ENVIRONMENT = 'master' 72 | envOpts.CONTENTFUL_LOCALE = 'en-US' 73 | 74 | const envText = Object.entries(envOpts).reduce( 75 | (all, [ name, data ]) => all.concat( 76 | `${name}=${ 77 | /\s/.test(data) 78 | ? JSON.stringify(data) 79 | : data 80 | }` 81 | ), 82 | [] 83 | ).join('\n') 84 | 85 | fs.writeFileSync(envPath, envText) 86 | 87 | Object.assign(meta.env, envValu) 88 | 89 | res.end(JSON.stringify(checkJSO)) 90 | } else { 91 | res.end(JSON.stringify({ sys: { type: 'Error' }})) 92 | } 93 | 94 | return 95 | } 96 | 97 | if (update.token && !update.space) { 98 | const checkURL = new URL('https://api.contentful.com/spaces') 99 | const checkReq = new Request(checkURL, { 100 | cache: 'no-cache', 101 | headers: { 102 | 'Accept': 'application/json', 103 | 'Authorization': 'Bearer ' + update.token, 104 | 'Accept-Encoding': 'gzip', 105 | 'User-Agent': 'node.js/', 106 | 'x-contentful-user-agent': 'sdk contentful-management.js/10.12.0;', 107 | }, 108 | }) 109 | 110 | res.setHeader('Content-Type', 'application/json;charset=utf-8') 111 | 112 | const checkRes = await fetch(checkReq) 113 | const checkJSO = await checkRes.json() 114 | 115 | if (checkJSO?.sys?.type === 'Array') { 116 | res.end(JSON.stringify(checkJSO)) 117 | } else { 118 | res.end(JSON.stringify({ sys: { type: 'Error' }})) 119 | } 120 | 121 | return 122 | } 123 | } catch (error) { 124 | res.end(JSON.stringify({ sys: { type: 'Error' } })) 125 | 126 | return 127 | } 128 | }); 129 | } 130 | 131 | return 132 | } 133 | 134 | if (!meta.env.CONTENTFUL_ACCESS_TOKEN) { 135 | res.writeHead(302, { 136 | Location: middlewareConfig.pathToHTML 137 | }).end() 138 | 139 | return 140 | } 141 | 142 | return next() 143 | }) 144 | 145 | return middleware 146 | } 147 | 148 | const middlewareConfig = { 149 | pathToHTML: '/@contentful/', 150 | pathToJSON: '/@contentful/api' 151 | } 152 | const pathToHtml = new URL('./contentful-middleware.html', import.meta.url) 153 | const touchFileSync = (/** @type {fs.PathLike} */ path) => { 154 | const time = new Date() 155 | 156 | try { 157 | fs.utimesSync(path, time, time) 158 | } catch (err) { 159 | fs.closeSync(fs.openSync(path, 'w')) 160 | } 161 | } 162 | 163 | /** @typedef {(req: Connect.IncomingMessage, res: ServerResponse, next: Connect.NextFunction) => void} ContentfulMiddleware */ 164 | /** @typedef {import('./createMeta.d').Meta} Meta */ 165 | -------------------------------------------------------------------------------- /packages/contentful/integrations/contentful-response.js: -------------------------------------------------------------------------------- 1 | export const contentfulResponse = async ( 2 | /** @type {string} */ input, 3 | /** @type {Meta} */ meta, 4 | /** @type {string} */ format 5 | ) => { 6 | const requestURL = new URL(input, 'https://api.contentful.com') 7 | 8 | const request = new Request(requestURL, { 9 | cache: 'no-cache', 10 | headers: { 11 | 'Accept': 'application/json', 12 | 'Authorization': 'Bearer ' + meta.env.CONTENTFUL_ACCESS_TOKEN, 13 | 'Accept-Encoding': 'gzip', 14 | 'User-Agent': 'node.js/', 15 | 'x-contentful-user-agent': 'sdk contentful-management.js/10.12.0;', 16 | }, 17 | }) 18 | 19 | /** Cached JSON response. */ 20 | let json = throttled[input] || null 21 | 22 | // cache the json response 23 | if (json === null) { 24 | const response = await fetch(request) 25 | 26 | json = throttled[input] = await response.json() 27 | 28 | setTimeout(() => { 29 | delete throttled[input] 30 | }, 200) 31 | } 32 | 33 | const isComponent = format === '!astro' 34 | 35 | const isEntry = requestURL.pathname.includes('/entries/') 36 | const isEntryList = requestURL.pathname.endsWith('/entries') 37 | 38 | const data = JSON.stringify(json, null, '\t') 39 | 40 | const code = ( 41 | isComponent && isEntry 42 | ? [ 43 | `import { renderTemplate, renderSlot } from 'astro/server/render/index.js';`, 44 | `import { richtexttohtml } from '@astropub/contentful';`, 45 | `export const locale = ${JSON.stringify(meta.env.CONTENTFUL_LOCALE)};`, 46 | `export const entry = ${data};`, 47 | toHTML, 48 | exportEntry, 49 | `export default Entry;`, 50 | ].join('\n') 51 | : isComponent && isEntryList 52 | ? [ 53 | `import { renderTemplate, renderSlot } from 'astro/server/render/index.js';`, 54 | `import { richtexttohtml } from '@astropub/contentful';`, 55 | `export const locale = ${JSON.stringify(meta.env.CONTENTFUL_LOCALE)};`, 56 | `export const entryList = ${data};`, 57 | toHTML, 58 | exportEntryList, 59 | `export default EntryList;`, 60 | ].join('\n') 61 | : `export default ${data};` 62 | ) 63 | 64 | return { code } 65 | } 66 | 67 | const throttled = Object.create(null) 68 | 69 | const toHTML = `const toHTML = (entry, name) => { 70 | let html = ''; 71 | 72 | html = entry?.[name]?.[locale] || null; 73 | html = html ? html.nodeType ? richtexttohtml(html) : html : null; 74 | 75 | return new String(html); 76 | };` 77 | 78 | const exportEntry = `export const Entry = async (result, attrs, slots) => { 79 | return renderTemplate\`\${ 80 | attrs.of in Object(entry.fields) 81 | ? toHTML(entry.fields, attrs.of) 82 | : renderSlot(result, slots.default) 83 | }\`; 84 | }; 85 | 86 | Entry.of = (of) => ( 87 | of in Object(entry.fields) 88 | ? toHTML(entry.fields, of) 89 | : null 90 | ); 91 | 92 | Entry.isAstroComponentFactory = true` 93 | 94 | const exportEntryList = `export const EntryList = (_result, attrs, slots) => { 95 | const render = slots.default?.().then( 96 | (result) => result.expressions.at(0), 97 | () => null 98 | ) 99 | 100 | return { 101 | get [Symbol.toStringTag]() { 102 | return 'AstroComponent' 103 | }, 104 | async *[Symbol.asyncIterator]() { 105 | const normalizedGenerator = getNormalizedGenerator(await render) 106 | 107 | if (entryList.items?.length) for (const itemdata of entryList.items) { 108 | const Entry = async (result, attrs, slots) => { 109 | return renderTemplate\`\${ 110 | attrs.of in Object(itemdata.fields) 111 | ? toHTML(itemdata.fields, attrs.of) 112 | : renderSlot(result, slots.default) 113 | }\`; 114 | }; 115 | 116 | Entry.of = (name) => ( 117 | name in Object(itemdata.fields) 118 | ? toHTML(itemdata.fields, name) 119 | : null 120 | ); 121 | 122 | Entry.isAstroComponentFactory = true 123 | 124 | yield * normalizedGenerator(Entry) 125 | } 126 | }, 127 | } 128 | }; 129 | 130 | EntryList.isAstroComponentFactory = true 131 | 132 | EntryList.None = async (result, attrs, slots) => { 133 | return renderTemplate\`\${ 134 | entryList.items === Object(entryList.items) 135 | ? '' 136 | : renderSlot(result, slots.default) 137 | }\`; 138 | }; 139 | 140 | EntryList.None.isAstroComponentFactory = true 141 | 142 | const isIterable = (value) => ( 143 | value != null && 144 | ( 145 | typeof value[Symbol.iterator] === 'function' || 146 | typeof value[Symbol.asyncIterator] === 'function' 147 | ) 148 | ) 149 | 150 | export const getNormalizedGenerator = (fn) => ( 151 | typeof fn !== 'function' 152 | ? async function * (value) { 153 | yield await value 154 | } 155 | : ( 156 | fn instanceof GeneratorFunction || 157 | fn instanceof AsyncGeneratorFunction 158 | ) 159 | ? fn 160 | : async function * (value) { 161 | yield await fn(await value) 162 | } 163 | ) 164 | 165 | const GeneratorFunction = (function * () {}).constructor 166 | 167 | const AsyncGeneratorFunction = (async function * () {}).constructor` 168 | 169 | /** @typedef {{ space: string, environment: string, origin: string, locale: string, token: string }} ContentfulConfig */ 170 | /** @typedef {import('./createMeta.d').Meta} Meta */ 171 | -------------------------------------------------------------------------------- /packages/contentful/integrations/contentful-vite-plugin.js: -------------------------------------------------------------------------------- 1 | import { contentfulResponse } from './contentful-response.js' 2 | 3 | export const contentfulVite = (/** @type {{ meta: Meta }} */ { meta }) => { 4 | /** Prefix used by this plugin to differentiate Contentful imports. */ 5 | const virtualModuleId = '@astropub/contentful:' 6 | 7 | /** Prefix used by Vite to differentiate virtual modules. */ 8 | const virtualPrefixId = '\0' 9 | 10 | return /** @type {VitePlugin} */ ({ 11 | name: 'vite:contentful', 12 | enforce: 'pre', 13 | resolveId(importeeId) { 14 | if (importeeId.startsWith(virtualModuleId)) { 15 | return virtualPrefixId + importeeId 16 | } 17 | }, 18 | async load(importeeId) { 19 | if (importeeId.startsWith(virtualPrefixId + virtualModuleId)) { 20 | const [ request, format ] = importeeId.slice(virtualModuleId.length + 1).split(/(?=\!astro$)/) 21 | 22 | return await contentfulResponse( 23 | `/spaces/${ 24 | meta.env.CONTENTFUL_SPACE 25 | }/environments/${ 26 | meta.env.CONTENTFUL_ENVIRONMENT 27 | }/${request}`, 28 | meta, 29 | format 30 | ) 31 | } 32 | }, 33 | }) 34 | } 35 | 36 | /** @typedef {import('./createMeta').Meta} Meta */ 37 | /** @typedef {import('./contentful-response.js').ContentfulConfig} ContentfulConfig */ 38 | /** @typedef {Partial & { space: string, token: string }} VitePluginConfig */ 39 | /** @typedef {import('vite').Plugin} VitePlugin */ 40 | -------------------------------------------------------------------------------- /packages/contentful/integrations/createMeta.d.ts: -------------------------------------------------------------------------------- 1 | export interface UpdateInit { 2 | /** Current working directory of the Node.js Process. */ 3 | cwd: URL 4 | 5 | /** Environment variables. */ 6 | env: EnvironmentVariables 7 | 8 | /** Mode the app is running in. */ 9 | mode: string 10 | } 11 | 12 | export interface Meta { 13 | /** Current working directory of the Node.js Process. */ 14 | cwd: URL 15 | 16 | /** Environment variables. */ 17 | env: EnvironmentVariables 18 | 19 | /** Mode the app is running in. */ 20 | mode: string 21 | 22 | /** Mode the server is running in. */ 23 | port: string 24 | 25 | /** Protocol the server is running as. */ 26 | protocol: string 27 | 28 | /** Hostname the server is running as. */ 29 | hostname: string 30 | 31 | /** Origin the server is running as. */ 32 | readonly origin: string 33 | 34 | update(init: URL): void 35 | } 36 | 37 | /** Environment variables. */ 38 | export interface EnvironmentVariables { 39 | [variable: string]: string 40 | } 41 | 42 | export declare function createMeta(): Meta 43 | -------------------------------------------------------------------------------- /packages/contentful/integrations/createMeta.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { fileURLToPath, pathToFileURL } from 'node:url' 4 | import { loadEnv } from 'vite' 5 | 6 | /** Returns context-specific metadata about the current process. */ 7 | export const createMeta = /** @type {() => Meta} */ () => { 8 | /** Current working directory of the Node.js Process. */ 9 | const cwd = pathToFileURL(process.cwd()) 10 | 11 | /** Environment variables. */ 12 | const env = /** @type {EnvironmentVariables} */ (Object.assign( 13 | Object.create(null), 14 | process.env 15 | )) 16 | 17 | /** Mode the app is running in. */ 18 | const mode = env.MODE || 'development' 19 | 20 | /** @type {Meta} */ 21 | const meta = { 22 | cwd, 23 | mode, 24 | port: env.PORT || '3000', 25 | protocol: env.PROTOCOL || 'http:', 26 | hostname: env.HOSTNAME || 'localhost', 27 | get env() { 28 | return env 29 | }, 30 | set env(value) { 31 | Object.assign(env, value) 32 | }, 33 | get origin() { 34 | return `${this.protocol}//${this.hostname}:${this.port}` 35 | }, 36 | /** Updates the internals. */ 37 | update(cwd) { 38 | // update the current working directory (`root`). 39 | this.cwd = cwd 40 | 41 | // clear the existing environment variables 42 | for (const name in env) { 43 | delete env[name] 44 | } 45 | 46 | // update the environment variables 47 | Object.assign(env, loadEnv(this.mode, fileURLToPath(this.cwd), '')) 48 | }, 49 | } 50 | 51 | return meta 52 | } 53 | 54 | /** @typedef {import('./createMeta.d').EnvironmentVariables} EnvironmentVariables */ 55 | /** @typedef {import('./createMeta.d').Meta} Meta */ 56 | /** @typedef {import('./createMeta.d').UpdateInit} UpdateInit */ 57 | -------------------------------------------------------------------------------- /packages/contentful/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/package.json", 3 | "name": "@astropub/contentful", 4 | "description": "Use Contentful in Astro", 5 | "version": "0.1.0", 6 | "type": "module", 7 | "license": "CC0-1.0", 8 | "exports": { 9 | ".": { 10 | "default": "./astro-contentful.js", 11 | "types": "./astro-contentful.d.ts" 12 | }, 13 | "./astro-contentful": { 14 | "default": "./astro-contentful.js", 15 | "types": "./astro-contentful.d.ts" 16 | }, 17 | "./global": { 18 | "types": "./global.d.ts" 19 | }, 20 | "./package": "./package.json", 21 | "./package.json": "./package.json" 22 | }, 23 | "main": "astro-contentful.js", 24 | "types": "astro-contentful.d.ts", 25 | "unpkg": "astro-contentful.js", 26 | "jsdelivr": "astro-contentful.js", 27 | "sideEffects": false, 28 | "files": [ 29 | "bundled", 30 | "integrations", 31 | "astro-contentful.js", 32 | "astro-contentful.d.ts" 33 | ], 34 | "keywords": [ 35 | "astro", 36 | "astro-component", 37 | "ui", 38 | "perf", 39 | "performance", 40 | "content", 41 | "contentful" 42 | ], 43 | "author": "Jonathan Neal ", 44 | "bugs": "https://github.com/astro-community/contentful/issues", 45 | "homepage": "https://github.com/astro-community/contentful", 46 | "repository": "https://github.com/astro-community/contentful.git" 47 | } 48 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "exactOptionalPropertyTypes": true, 7 | "isolatedModules": true, 8 | "resolveJsonModule": true, 9 | "strictNullChecks": true, 10 | "types": ["astro/client", "types-object"] 11 | } 12 | } 13 | --------------------------------------------------------------------------------