Skip to content

Commit

Permalink
Remove support for custom responses (#1340)
Browse files Browse the repository at this point in the history
* Convert sitemap to API function

* Convert robots.txt to API function

* Use js instead of jsx extension

* Remove custom body and response.send logic

* Remove docs about custom responses

* Remove tests related to custom body

* Add changeset

* Run prettier lol

* Remove old JSX references

* Remove async from functions that no longer need it

* Remove x from filename
  • Loading branch information
jplhomer authored May 25, 2022
1 parent fef4cb8 commit 631832e
Show file tree
Hide file tree
Showing 9 changed files with 32 additions and 369 deletions.
5 changes: 5 additions & 0 deletions .changeset/wet-ways-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/hydrogen': minor
---

The `response.send()` function has been removed. Use `export async function api()` to send custom responses instead.
285 changes: 0 additions & 285 deletions docs/framework/routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,26 +379,6 @@ return response.redirect('https://yoursite.com/new-page', 301);
> Caution:
> You must call `return response.redirect()` before any calls to `fetchSync`, `useQuery` or `useShopQuery` to prevent streaming while the Suspense data is resolved, or use `response.doNotStream()` to prevent streaming altogether on the response. The value must also be returned.
#### `response.send()`

If you want to return a different response body than React-rendered HTML, then pass the custom body to `response.send()` and return it from your server component:

{% codeblock file %}

```jsx
export default function CustomPage({response}) {
response.doNotStream();

response.headers.set('content-type', 'application/json');

return response.send(JSON.stringify({data: 'here'}));
}
```

{% endcodeblock %}

Since this code lives inside a server component, you can use [`useShopQuery`](https://shopify.dev/api/hydrogen/hooks/global/useshopquery) to populate your [custom responses](#custom-responses) with Shopify data.

### Server props

In addition to `request` and `response` props, any props you manage with [`setServerProps`](https://shopify.dev/custom-storefronts/hydrogen/framework/server-props) is passed to each of your server components as props:
Expand All @@ -413,271 +393,6 @@ function MyPage({custom, props, here}) {

{% endcodeblock %}

## Custom responses

Custom responses are React components that you can use to compose complex functionality in a response. This section provides examples that show some creative ways to use custom responses in your Hydrogen app.

Custom responses provide the following benefits:

- You don't have to use a custom API function to respond with JSON or another format.
- You don't need to use another file or a different pattern to respond with something that's not a standard HTML response.
- You have complete freedom over the response.

### Create a custom sitemap

The following example shows how to create a custom sitemap by adding a new server component called `routes/sitemap.xml.server.jsx`. The custom response object returns the sitemap.

{% codeblock file, filename: '/routes/my-products.server.jsx' %}

```jsx
import {flattenConnection, useShopQuery, gql} from '@shopify/hydrogen';

export default function Sitemap({response}) {
response.doNotStream();

const {data} = useShopQuery({query: QUERY});

response.headers.set('content-type', 'application/xml');

return response.send(shopSitemap(data));
}

function shopSitemap(data) {
return `
<urlset
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
>
${flattenConnection(data.products)
.map((product) => {
return `
<url>
<loc>
https://hydrogen-preview.myshopify.com/products/${product.handle}
</loc>
<lastmod>${product.updatedAt}</lastmod>
<changefreq>daily</changefreq>
<image:image>
<image:loc>
${product?.images?.edges?.[0]?.node?.url}
</image:loc>
<image:title>
${product?.images?.edges?.[0]?.node?.altText ?? ''}
</image:title>
<image:caption />
</image:image>
</url>
`;
})
.join('')}
</urlset>`;
}
const QUERY = gql`
query Products {
products(first: 100) {
edges {
node {
updatedAt
handle
featuredImage {
url
altText
}
}
}
}
}
`;
```
{% endcodeblock %}
### Build a JSON API
In modern app frameworks, it's common to create custom API endpoints in your own framework powered by the hosting platform you're using. In other frameworks, these API endpoints provide helpful ways to handle lazy-loading, Ajax type incremental data, or POST requests to mutate an external data store. For example, you might want to send a POST request to write to a custom data store after submitting a form.
The following example shows how to build a JSON API with custom responses by adding a new server component called `/routes/my-products.server.jsx`. The custom response object returns the JSON API:
{% codeblock file, filename: '/routes/my-products.server.jsx' %}
```jsx
import {flattenConnection, useShopQuery, gql} from '@shopify/hydrogen';
export default function MyProducts({response}) {
response.doNotStream();
const {data} = useShopQuery({query: QUERY});
response.headers.set('content-type', 'application/json');
return response.send(JSON.stringify(flattenConnection(data.products)));
}
const QUERY = gql`
query Products {
products(first: 100) {
edges {
node {
updatedAt
handle
featuredImage {
url
altText
}
}
}
}
}
`;
```
{% endcodeblock %}
### Generate a spreadsheet
You might want to generate a spreadsheet that includes product data from your store.
The following example shows how to generate comma-separated values (CSV) file by adding a new server component called `/routes/spreadsheet.csv.server.jsx`. The custom response object returns the spreadsheet:
{% codeblock file, filename: '/routes/spreadsheet.csv.server.jsx' %}
```jsx
import {flattenConnection, useShopQuery, gql} from '@shopify/hydrogen';
export default function Report({response}) {
response.doNotStream();
const {data} = useShopQuery({query: QUERY});
response.headers.set('content-type', 'application/csv');
return response.send(
flattenConnection(data.products)
.map((product) => [product.id, product.title, product.handle].join(','))
.join('\n')
);
}
const QUERY = gql`
{
products(first: 10) {
edges {
node {
id
title
handle
}
}
}
}
`;
```
{% endcodeblock %}
### Generate PDFs
You might want to generate brochures for products in a Shopify store.
The following example shows how to generate a downloadable PDF for a product in a store by installing `@react-pdf/renderer`:
```bash
yarn add @react-pdf/renderer
```
After you've installed `@react-pdf/renderer`, create a new server component called `/routes/brochure.pdf.server.jsx`:
{% codeblock file, filename: '/routes/brochure.pdf.server.jsx' %}
```jsx
import {
Page,
Text,
View,
Document,
StyleSheet,
renderToString,
Image,
} from '@react-pdf/renderer';
import {useShopQuery, gql} from '@shopify/hydrogen';
const styles = StyleSheet.create({
page: {
flexDirection: 'row',
backgroundColor: '#E4E4E4',
},
section: {
margin: 10,
padding: 10,
flexGrow: 1,
width: '50%',
},
description: {
fontSize: 10,
},
});
export default function Brochure({response}) {
response.doNotStream();
const {data} = useShopQuery({query: QUERY});
const product = data.productByHandle;
const BrochureDocument = () => (
<Document>
<Page size="A4" style={styles.page}>
<View style={styles.section}>
<Text>{product.title}</Text>
<Text style={styles.description}>{product.description}</Text>
</View>
<View style={styles.section}>
<Image src={product.images.edges[0].node.url} />
</View>
</Page>
</Document>
);
response.headers.set('content-type', 'application/pdf');
return response.send(renderToString(<BrochureDocument />));
}
const QUERY = gql`
{
productByHandle(handle: "snowboard") {
title
handle
description
featuredImage {
url
}
}
}
`;
```
{% endcodeblock %}
### Interacting with custom responses on the browser
When you build custom responses, you might want to call them from the browser using `fetch`. For example, you could check an API endpoint like `/api/views`.
To call a custom response from the client, you need to tell Hydrogen about the request using a custom `accept` header value called `application/hydrogen`. You can combine this header value with any other `accept` value. This tells Hydrogen to handle the response using a server component rather than attempting to load an asset:
{% codeblock file %}
```js
await fetch('/api/views', {
headers: {
accept: 'application/hydrogen, application/json',
},
});
```
{% endcodeblock %}
## Related components and hooks

- [`Link`](https://shopify.dev/api/hydrogen/components/framework/link)
Expand Down
6 changes: 3 additions & 3 deletions docs/framework/seo.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ Hydrogen includes an [`Seo`](https://shopify.dev/api/hydrogen/components/primiti

- [`DefaultSeo`](https://github.com/Shopify/hydrogen/blob/main/templates/template-hydrogen-default/src/components/DefaultSeo.server.jsx): A server component that fetches the shop name and description and sets default values and templates for every page on a website

- [`Sitemap.xml.server.jsx`](https://github.com/Shopify/hydrogen/blob/main/templates/template-hydrogen-default/src/routes/sitemap.xml.server.jsx): A file that generates all products, collections, and pages URLs using the Storefront API
- [`Sitemap.xml.server.js`](https://github.com/Shopify/hydrogen/blob/main/templates/template-hydrogen-default/src/routes/sitemap.xml.server.js): A file that generates all products, collections, and pages URLs using the Storefront API

- [`Robots.txt.server.jsx`](https://github.com/Shopify/hydrogen/blob/main/templates/template-hydrogen-default/src/routes/robots.txt.server.js): A file that sets default rules for which URLs can be crawled by search engines
- [`Robots.txt.server.js`](https://github.com/Shopify/hydrogen/blob/main/templates/template-hydrogen-default/src/routes/robots.txt.server.js): A file that sets default rules for which URLs can be crawled by search engines

### `Seo` client component

Expand Down Expand Up @@ -156,7 +156,7 @@ To imitate the behaviour of a SEO robot and show the page content fully from ser

## Limitations and considerations

The following limitations and considerations apply to the [XML sitemap](https://github.com/Shopify/hydrogen/blob/main/templates/template-hydrogen-default/src/routes/sitemap.xml.server.jsx) that's included in the Demo Store template:
The following limitations and considerations apply to the [XML sitemap](https://github.com/Shopify/hydrogen/blob/main/templates/template-hydrogen-default/src/routes/sitemap.xml.server.js) that's included in the Demo Store template:

- The sitemap has a limit of 250 products, 250 collections, and 250 pages. You need to [paginate results](https://shopify.dev/api/usage/pagination-graphql) if your store has more than 250 resources.

Expand Down
28 changes: 3 additions & 25 deletions packages/hydrogen/src/entry-server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -256,19 +256,6 @@ async function render(
* we want to shard our full-page cache for all Hydrogen storefronts.
*/
headers.set('cache-control', componentResponse.cacheControlHeader);

if (componentResponse.customBody) {
// This can be used to return sitemap.xml or any other custom response.

postRequestTasks('ssr', status, request, componentResponse);

return new Response(await componentResponse.customBody, {
status,
statusText,
headers,
});
}

headers.set(CONTENT_TYPE, HTML_CONTENT_TYPE);

html = applyHtmlHead(html, request.ctx.head, template);
Expand Down Expand Up @@ -383,7 +370,7 @@ async function stream(
});

/* eslint-disable no-inner-declarations */
async function prepareForStreaming(flush: boolean) {
function prepareForStreaming(flush: boolean) {
Object.assign(
responseOptions,
getResponseOptions(componentResponse, didError)
Expand All @@ -404,11 +391,6 @@ async function stream(
}

if (flush) {
if (componentResponse.customBody) {
writable.write(encoder.encode(await componentResponse.customBody));
return false;
}

responseOptions.headers.set(CONTENT_TYPE, HTML_CONTENT_TYPE);
writable.write(encoder.encode(DOCTYPE));

Expand All @@ -423,7 +405,7 @@ async function stream(
/* eslint-enable no-inner-declarations */

const shouldReturnApp =
(await prepareForStreaming(componentResponse.canStream())) ??
prepareForStreaming(componentResponse.canStream()) ??
(await onCompleteAll.promise.then(prepareForStreaming));

if (shouldReturnApp) {
Expand Down Expand Up @@ -522,7 +504,7 @@ async function stream(
return response.write(chunk);
});
},
async onAllReady() {
onAllReady() {
log.trace('node complete stream');

if (componentResponse.canStream() || response.writableEnded) {
Expand All @@ -549,10 +531,6 @@ async function stream(
return response.end();
}

if (componentResponse.customBody) {
return response.end(await componentResponse.customBody);
}

startWritingHtmlToServerResponse(response, dev ? didError : undefined);

bufferReadableStream(rscToScriptTagReadable.getReader()).then(
Expand Down
Loading

0 comments on commit 631832e

Please sign in to comment.