Encoding static imagery from public folder using getStaticProps in Next.js - javascript

Next.js lays out a pretty comprehensive way to get imagery from the /public/ folder (where the app has you store static assets). The pattern is to use fs from Node and do the fetch in getStaticProps.
My attempt:
export async function getStaticProps({ params, preview = false, previewData }) {
const cityData = dataFiltered.find( city => city.citySlug === params.slug )
const cityMapImagePath = path.join(process.cwd(), `/public/static-maps/${cityData.imgPath}`)
const cityMapImageRes = await fs.readFile(cityMapImagePath)
const cityMapImageProcessed = JSON.stringify(cityMapImageRes)
return {
props: {
preview,
cityData: cityData,
cityMapImage: cityMapImageProcessed
},
revalidate: 60,
};
}
This code works, but it returns a pretty weird response when I reference that object in my component:
<img src="{ "type":"Buffer", "data":[255,216,255,224,0,6,75,56,86,87,...] } />
My error has something to do with how I'm processing what fs gives me back. Do I need to encode my jpeg into base64 to get Next to use it? This answer suggests stringifying and then parsing (didn't work for me). Or maybe I need a full blown endpoint to do this? Next isn't very clear on how to get imagery from getStaticProps into the component above it - perhaps you know how?

All data that is returned from the getStaticProps needs to be JSON serializable, so yes, if you want to return image there , you need to base64 encode it (this can be a problem for big images).
The other solution (if the scenario permits it) is not to do it with getStaticProps rather load the image on demand in the front end, by hitting the API after the page has already loaded.

What I ended up doing for the fetch in getStaticProps:
export async function getStaticProps({ params, preview = false, previewData }) {
const cityData = dataFiltered.find( city => city.citySlug === params.slug )
const cityMapImagePath = path.join(process.cwd(), `/public/static-maps/${cityData.imgPath}`)
let cityMapImageRes, cityMapImageProcessed
try {
cityMapImageRes = await fs.readFile(cityMapImagePath)
cityMapImageProcessed = Buffer.from(cityMapImageRes).toString('base64')
} catch {
cityMapImageProcessed = null
}
return {
props: {
preview,
cityData: cityData,
cityMapImage: cityMapImageProcessed
},
revalidate: 60,
};
}
Also make sure up in the component, you are properly encoding your image source as base64 with the data:image/png;base64, prefix. This was a silly mistake that cost me an hour of debugging:
<img src={`data:image/png;base64,${cityMapImage}`} alt='Alt text here' />
Finally, also note that Next.js when used with Vercel will impose a hard, 50MB cap (compressed) on the serverless function used to process your page file request. If you're doing a [slug].js template, all the assets for EVERY page generated will count towards that cap. You'll hit it pretty fast on deploy, so double check yourself.

Related

Redrects with special characters in Next.js

I'm setting up redirects for a client in Next.js. We have a configuration like so inside next.config.js:
{
source: '/page.aspx:urlstr*',
destination: 'https://someurl.com/page.aspx:urlstr*',
permanent: true,
},
The actual URLs hitting this will have a long URL string like so:
page.aspx?woXxJNrRIMKVC109awwTopP+k2NmkvXf+MzijTEc3zIZ3pf4n+Yknq
This is being URL encoded to be:
https://someurl.com/page.aspx?woXxJNrRIMKVC109awwTopP%20k2NmkvXf%20MzijTEc3zIZ3pf4n%20Yknq
The old server hosting these pages at the destination can't handle the URL encoded query string. Is there a way to force Next.js to not parse it?
So the solution was in fact to use the Next.js middleware feature:
https://nextjs.org/docs/advanced-features/middleware
It's a little buggy though. The docs say to add middleware.js to the same level as the pages directory, but you actually need to add _middleware.js inside the pages directory.
Also the matches feature does not seem to work for me at all, so here's my solution:
import { NextResponse } from 'next/server'
export function middleware(request) {
if (request.nextUrl.pathname.startsWith('/page.aspx')) {
let url = new URL(request.url);
return NextResponse.redirect(`https://someurl.com${url.pathname}${url.search}`)
}
}

Next.js – encodeURIComponent doesn't work with `/` in `getStaticPaths`

Minimal repro site: https://nextjs-paths-issue.vercel.app/
Minimal repro code: https://github.com/saadq/nextjs-encoding-issue
Home page
Food page
I am trying to iterate through an array of food objects and create static pages for each one based on its title. This works for most of the foods, but if the food title contains a /, then navigating to the page (such as the "Nice strawberry/kiwis dessert" page) will throw a 404.
In the home page, I encode the URL when I create the Link and then in the getStaticPaths function, I create the paths using the same encoded link. However, it doesn't seem to work when deployed.
The page does work locally when running npm run dev, but it seems that in the actual output build there are issues. Is there something I can do to allow paths with encoded slashes to work?
Home page
const HomePage: NextPage = () => (
<>
<h1>Home</h1>
<ul>
{foods.map((food) => (
<li key={food.title}>
<Link href={`/food/${encodeURIComponent(food.title)}`}>
{food.title}
</Link>
</li>
))}
</ul>
</>
)
Food page
export const getStaticProps: GetStaticProps<Props, Params> = (ctx) => {
const title = ctx.params?.foodTitle as string
const food = foods.find((food) => food.title === title) as Food
return {
props: {
food
}
}
}
export const getStaticPaths: GetStaticPaths = () => {
const paths = foods.map((food) => `/food/${encodeURIComponent(food.title)}`)
return {
paths,
fallback: false
}
}
const FoodPage: NextPage<Props> = ({ food }) => {
return (
<>
<Link href='/'>Go Back</Link>
<h1>{food.title}</h1>
<h2>Amount: {food.amount}</h2>
</>
)
}
export default FoodPage
The example I posted here is a bit contrived, for my actual app I was able to get it to work by using fallback: 'blocking'. It's not a totally exportable static website anymore unfortunately, but I only have a few pages that will run into this issue so it's fine that a few of them will have a slight loading time from the server.
https://nextjs.org/docs/basic-features/data-fetching
// We'll pre-render only these paths at build time.
// { fallback: blocking } will server-render pages
// on-demand if the path doesn't exist.
return { paths, fallback: 'blocking' }
An old thread, but I've spent many hours looking for a solution so I'd like to share.
In order to get links in the format /categories/page in getStaticPath, and at the same time to use the same component in the /category/ and in the /category/page/, but with different parameters, you should use the page format [...category].js. This allows you to pass parameters in getStaticPaths as an array:
params: {
category: [category, page],
}
In result it will generate page category/page and still have fallback: false.
Trying to add '/' directly to the path (as you did) creates html pages category%2fpage.html.
More details about static paths
And about catch all routes
Ofc, because you have / in the path. %2F symbol representing /
Just make custom function for path encoding and use it
static stringToParamUrl(input: string) {
let url = input.normalize("NFD").toLowerCase().replace(/[\u0300-\u036f]/g, "").replace(/[^a-zA-Z0-9-_]/g, "-")
return url
}
Modifications:
"I create the paths using the same encoded link. However, it doesn't seem to work when deployed." if it's worked it is not correct. Your other links are working exept those containing "/". "/" is a reserved character for the URL. In next js it's working as "separator".
Also (about Link):
You need to have <a> tag in Link
<Link><a>something</a></Link>
Also (about fallback):
Fallback is not changing the logic of URLs. Using fallback will generate static page on the "first demand" - nothing about URL not working.
You had no errors during deployment time because you didn't create static page (fallback false)
What you need:
"I am trying to iterate through an array of food objects and create static pages for each one based on its title." = you need to add to your database a field called "URL" or something like this, and iterate elements based on URL *you previously generated while creating your array) and not name, because:
You need to have "unique" URL (to get data from your api via [pid] parameters or something)
You need to avoid reserved and forbidden symbols in URL
It's better to prettify url. ( I shared a function with you)
Maybe I didn't understand your question correctly

How to upload a file in React?

I'm trying to upload a jpeg file (2.1Mb) using react-images-upload, however, when I save the received file on backend, it has 7.5 Mb. Could anyone give me suggestions to understand what's wrong?
<ImageUploader
withIcon={true}
buttonText="Choose the Document"
onChange={onDrop}
maxFileSize={5242880}
idealResolution={{ width: 640, height: 480 }}
isMaxResolution={true}
/>
and the handle function:
const onDrop = (documentFiles, documentDataURLs) => {
const document = documentFiles.pop();
document.arrayBuffer().then((arrayBuffer) => {
dispatch(
submitDocument(
[{ name: document.name, bytes: new Uint8Array(arrayBuffer) }],
submitDocumentCallback
)
);
});
};
In the backend, I'm justing saving the bytes using a FileOutputStream.
I looked at the source code for react-images-upload and have to say that it doesn't look like something I'd want to put in production.
I also don't see how you actually make it upload to the server. Your example doesn't include where you specify the server URL.
For uploading files in React, I'd go with something more robust. I'm biased toward react-uploady (since it's my project). So I'd recommend it. But there are other good ones as well (I guess).
For example, with react-uploady, its as simple as:
import React from "react";
import Uploady from "#rpldy/uploady";
import UploadButton from "#rpldy/upload-button";
const MyApp = () => (
<Uploady destination={{url: "https://my-server.com/upload"}}>
<UploadButton/>
</Uploady>
);

React Filepond Loading Initial Images Not Working

I'm having some trouble using Filepond for React. Currently, I want to preload a user's profile picture when the page is loaded. I have tried two different approaches but none have worked yet:
First, I try to use the server load option of Filepond like so:
<FilePond
ref={pond}
... other props here
files={[
{
source: this.props.user.id,
options: {
type: 'local'
}
}
]}
server={{
load: (userId, load) => {
fetch(fetchDomain + "/profiles/" + userId)
.then(res => res.blob())
.then(blob => {
console.log('blob is: ', blob)
return load
})
}
}}
/>
I fetch from my express backend which is running on the same domain and returns an s3 image URL. This leads to an infinite loading situation and a picture is never loaded. I kept making small changes to this trying to fix it, but was never able to get it to work. At some point when I was messing with the code it would finish loading, but the image would be gray/unavailable with no error messages.
Another approach I took was simply setting Files to an image URL and this didn't work either. Same result, a greyed out image that I can clear it by clicking the remove item button. I did notice, however, that if that URL was a base64 url it would actually load the image properly.
I've been debugging this for a few hours already. Is there something I am missing here?
I'm not sure if it's relevant, but the setup at the top of my file looks like:
import 'filepond/dist/filepond.min.css';
import CropperEditor from './Cropper';
import FilePondPluginFileValidateType from "filepond-plugin-file-validate-type";
import FilePondPluginImageExifOrientation from "filepond-plugin-image-exif-orientation";
import FilePondPluginImagePreview from "filepond-plugin-image-preview";
import FilePondPluginImageCrop from "filepond-plugin-image-crop";
import FilePondPluginImageResize from "filepond-plugin-image-resize";
import FilePondPluginImageTransform from "filepond-plugin-image-transform";
import FilePondPluginImageEdit from "filepond-plugin-image-edit";
import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css';
const fetchDomain = process.env.NODE_ENV === 'production' ? process.env.REACT_APP_FETCH_DOMAIN_PROD : process.env.REACT_APP_FETCH_DOMAIN_DEV;
// Register the plugins for filepond
registerPlugin(
FilePondPluginFileValidateType,
FilePondPluginImageExifOrientation,
FilePondPluginImagePreview,
FilePondPluginImageCrop,
FilePondPluginImageResize,
FilePondPluginImageTransform,
FilePondPluginImageEdit,
);
const pond = React.createRef();
Here is a picture of what the component looks like when loading after I got rid of all the plugins apart from FilePondPluginImagePreview and use the first method:
It says undefined...
After further reading of the documentation I'm now confused about whether I should be loading or fetching.
I was understanding the load and fetch features of the server object incorrectly. I had to get the image URL before and then use fetch or load within the server object in order to get the file from that URL and then convert it to a blob format. This would also work with an external server endpoint being called within the load feature of the server object as I was trying to do initially, but you'd have to make sure the server returns a file, not a URL. Well, I guess it could return a URL but you'd then have to do another fetch on that URL and then convert it to blob.
The answer to this question is what I did essentially.
File Preview in React FilePond not showing up when setting initial file

Upload file to Google Drive with React and Google App script

I am trying to upload file with Google App script and React.
Google Script:
function uploadArquivoParaDrive(base64Data, nomeArq, idPasta) {
try{
var splitBase = base64Data.split(','),
type = splitBase[0].split(';')[0].replace('data:','');
var byteCharacters = Utilities.base64Decode(splitBase[1]);
var ss = Utilities.newBlob(byteCharacters, type);
ss.setName(nomeArq);
var file = DriveApp.getFolderById(idPasta).createFile(ss);
return file.getName();
}catch(e){
return 'Erro: ' + e.toString();
}
}
I can run this ant it works:
function uploadFile() {
var image = UrlFetchApp.fetch('url to some image').getBlob();
var file = {
title: 'google_logo.png',
mimeType: 'image/png'
};
file = Drive.Files.insert(file, image);
Logger.log('ID: %s, File size (bytes): %s', file.id, file.fileSize);
}
This is React script:
onSubmit = (e) => {
e.preventDefault();
axios.get(url, {...this.state}, {
headers: {
'Content-Type': 'multipart/form-data'
}
}, (response) => {
console.log(response);
})
};
setFile = (event) => {
console.log(event.target.files)
this.setState({file: event.target.files[0]});
};
render() {
return (
<form>
<input type="file" id="file" onChange={this.setFile} />
<button onClick={this.onSubmit}>ADD</button>
</form>
)
}
I am trying with POST, but I am getting 400 response. I know that this can't be GET request, but and with it, I am getting - 200 with no response.
I can insert rows in sheets, but I want to upload files to Google Drive with Google App Scripts.
I know that there is a way to upload files via Google Scripts and React, because, there is a way without React (google.script.run).
Here are two different approaches that are used in mixed mode. It's unacceptable in some contexts.
Let's say softly 'React is a dummy'. This is an add-on that you should always avoid when somewhat comes to something that you depend on, but you cannot change. See what SOLID is.
Below it is always assumed that you are working in a browser. Your web-pages are hosted in the web application of the Google Apps Script.
The first approach. Using XMLHttpRequests
On client side you have to use XMLHttpRequests call from your browser.
On server side you have to use doGet doPost reserved functions. Always transfer data in a clear and simple format. This will save time searching for errors.
Example https://stackoverflow.com/a/11300412/1393023
The second approach. Using Client-side API
On client side you have to use google.script.run call from your browser.
On server side you have to use your functions. Always transfer data in a clear and simple format. This will save time searching for errors.
Example https://stackoverflow.com/a/15790713/1393023
Consequence
Your example has signs of mixing approaches. Unfortunately, it cannot be quickly debugged.
There is no reason that React is causing the problem. If so, then your architecture is incorrect.
If you want to use axios, then you need to consider the first approach.
If you want to use google.script.run then you need to catch onSubmit then you need to call an interface that implement google.script.run. Usually asynchronously, since the last call will still be completed with a callback.

Categories

Resources