I need help building a Sitemap for my NextJs project.
I build a headless cms using graphql and next, however, everything is statically generated.
I'm having a lot of issues creating a sitemap. I tried using the npm next-sitemap but all the info I find (youtube and forums) are for projects containing "serversideprops", when my project only contains "getStaticProps" and getStaticPaths. In addition to that I also require the map to handle dynamic paths [slug].js. ** I'm not using typescript
Here is what part of my [slug].js looks like:
> graphql query....
>
> export async function getStaticPaths() { const { posts } = await
> graphcms.request(SLUGLIST); return {
> paths: posts.map((post) => ({ params: { slug: post.slug } })),
> fallback: false, }; }
>
> export async function getStaticProps({ params }) { const slug =
> params.slug; const data = await graphcms.request(QUERY, { slug });
> const { posts } = await graphcms.request(QUERY2); const post =
> data.post; return {
> props: {
> post,
> posts,
> }, }; }
thanks everyone!
next-sitemap also creates the routes based on the static pages you generate. The thing is you have to run it after you generated your project.
Typically, you should have a configuration file like this at the base of your project:
next-sitemap.config.js
/** #type {import('next-sitemap').IConfig} */
module.exports = {
siteUrl: process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : 'https://my-fallback-url.com/',
generateRobotsTxt: true,
trailingSlash: true,
targetDirectory: `${__dirname}/public`,
// Wherever are your pages stored
pagesDirectory: `${__dirname}/src/pages`,
};
and on your package.json
"scripts": {
... other configurations
"postbuild": "next-sitemap"
},
Which will trigger next-sitemap after your build is complete. You should then find all the generated xml files containing your sitemap.
Related
I'm trying to add META tags for Facebook(og-tags). I'm using Gatsby and Helmet. But the problem is that those tags first need to be fetched.
It's a vehicle detail page and I need to show vehicle make and model in those tags, but the vehicle needs first to be fecthed. My code is as follows:
import Helmet from 'react-helmet';
const Page = (props) => {
const [detailsMeta, setDetailsMeta] = useState(undefined);
const resolveVehicleDetailMeta = async () => {
const fetch = require('isomorphic-fetch');
const resolveVehicleImageUrl = (fetchedImage) => {
const parsed = JSON.parse(fetchedImage);
return parsed?.uri
}
const VEHICLE_QUERY = `
query VehicleQuery($reference: String!) {
vehicle (reference: $reference) {
reference
make
model
image
}
}`;
await fetch(`/graphql`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: VEHICLE_QUERY,
variables: {
reference: 'some reference'
}
})
})
.then((resp) => resp.json())
.then((result) => {
const vehicle = result?.data?.vehicle;
if(vehicle){
setDetailsMeta({
reference: vehicle.reference,
make: vehicle.make,
model: vehicle.model,
image: resolveVehicleImageUrl(vehicle.image)
})
}
})
.catch((err) => {
console.log('err', err)
});
}
const renderMetaTitle = () => {
const ogTitle = `Tweedehands ${detailsMeta?.make} ${detailsMeta?.model} ${detailsMeta?.reference}`
return ogTitle;
}
return (
<>
<Helmet>
{detailsMeta && <meta property='og:title' content={renderMetaTitle()} />}
...
</Helmet>
The rest...
</>
)
}
And in gatsby config is gatsby-plugin-react-helmet added. The config file is as follows:
const plugins = [
`gatsby-plugin-react-helmet`,
....
]
module.exports = {
developMiddleware: (app) => {
app.use(
'/graphql',
createProxyMiddleware({
target: 'http://localhost:8000'
})
);
},
siteMetadata: {
siteUrl: `https://${settings.DOMAIN}`
},
plugins: plugins
};
Thus, I first fetch data from the server and store it in detailsMeta and then I show it inside Helmet. When I test it on localhost I see those tags and it works fine, but when I test it in Facebook debugger they are not shown.
Can I at all add data to the og-tags which first needs to be fetched and that it be seen by Facebook?
Thanks.
Can I at all add data to the og-tags which first needs to be fetched
and that it be seen by Facebook?
Yes but only if you are using statically analyzed data (i.e: using page queries, static queries, etc). In that case, you just need to add gatsby-plugin-react-helmet plugin in order to add drop-in server-side support to Helmet component.
In your gatsby-config.js:
plugins: [`gatsby-plugin-react-helmet`]
Extracted from https://www.gatsbyjs.com/plugins/gatsby-plugin-react-helmet/
In your case, you are using the fetch method that will be fired on the client-side, so the data won't be statically analyzed hence not present when the Facebook crawler reaches the site. This means that the Helmet component will be populated later than the crawler requests it.
I'm not sure about your specs but you can try converting your fetched into GraphQL nodes in order to use pages queries or static queries fulfill the Helmet component properly.
I have trouble understanding the dynamic URL routing to match my use case, and the examples only showcase structure like : domain.com/categories/category1, domain.com/posts/post-1.
As the title says, I would like to have the following URL structures on my blog, with Next.js :
https://www.example.com/category1/post-1
https://www.example.com/category1/post-2
https://www.example.com/category1/post-3
https://www.example.com/category2/post-4
https://www.example.com/category2/post-5
...
I would also like the pages :
https://www.example.com/category1
https://www.example.com/category2
to be accessible.
So far, my file structure looks like this : but I'm getting 404 errors.
Thank you for your help
If that can be useful to someone else, I've found the solution.
The folder structure has to be like this :
pages
[category]
[slug.tsx] // (post page)
index.tsx // (category page)
In the [slug.tsx] page, your static paths must reflect the structure :
export const getStaticPaths: GetStaticPaths = async () => {
const allPosts = await getAllPostsWithSlug();
return {
paths: allPosts?.map((post) => `/${post.category.slug}/${post.slug}`) || [],
fallback: true,
};
};
In the index.tsx (category) page, likewise :
export const getStaticPaths: GetStaticPaths = async () => {
const allCategories = await getAllCategories();
return {
paths: allCategories.map((category) => `/${category.slug}`) || [],
fallback: true,
};
};
With this, I can access my posts with their category in the url.
Hope you all are having a good time. I am working on a simple NextJs application where I want to have multiple subdomains in it. I am deploying the application on vercel.
What my application does is it have a simple textarea where you write MDX, you click the Publish button and it will save that MDX into firebase firestore. Below the textarea it shows the list of all the pages that have been published before.
The application renders the list of all the pages like with name of the page which is randomly generated as the subdomain while the actual domain comes later like the following.
a-mdx-page.mydomain.app
When I open that URL it will fetch the page MDX from firestore and uses next-mdx-remote package to serialize and render the MDX. The reason for using the next-mdx-remote package is that we can add react components in MDX and it can render as normal react components. I already have a custom domain because you cannot have a subdomain on top of a free subdomain in vercel free deployment.
All works fine on localhost and everything is as it should be but the problem is when I deploy the code on Vercel and navigate to subdomain it shows ERROR 500 on the website and shows the following error in the logs.
[GET] / 21:21:03:30
2021-10-24T16:21:04.018Z 8e52d5da-ff1f-4840-a09b-199233834a5d ERROR Error: The package "esbuild-linux-64" could not be found, and is needed by esbuild.
If you are installing esbuild with npm, make sure that you don't specify the
"--no-optional" flag. The "optionalDependencies" package.json feature is used
by esbuild to install the correct binary executable for your current platform.
at generateBinPath (/var/task/node_modules/esbuild/lib/main.js:1643:15)
at esbuildCommandAndArgs (/var/task/node_modules/esbuild/lib/main.js:1699:11)
at ensureServiceIsRunning (/var/task/node_modules/esbuild/lib/main.js:1856:25)
at Object.transform (/var/task/node_modules/esbuild/lib/main.js:1751:37)
at serialize (/var/task/node_modules/next-mdx-remote/dist/serialize.js:287:43)
at async getServerSideProps (/var/task/.next/server/pages/index.js:261:25)
at async Object.renderToHTML (/var/task/node_modules/next/dist/server/render.js:428:24)
at async doRender (/var/task/node_modules/next/dist/server/next-server.js:1144:38)
at async /var/task/node_modules/next/dist/server/next-server.js:1236:28
at async /var/task/node_modules/next/dist/server/response-cache.js:64:36 {
page: '/'
}
RequestId: 8e52d5da-ff1f-4840-a09b-199233834a5d Error: Runtime exited with error: exit status 1
Runtime.ExitError
From what I understand that the next-mdx-remote serialize function uses esbuild in it and when I deploy the application on vercel npm just doesn't downloads the platform specific package of it but may be I am wrong.
I have tried to search the solution for this but there is not any answers that helped me.
Following is all the code that the application uses.
import { useState } from "react"
import { collection, doc, getDoc, getDocs, setDoc } from "firebase/firestore"
import matter from "gray-matter"
import { MDXRemote } from "next-mdx-remote"
import { serialize } from "next-mdx-remote/serialize"
import {
uniqueNamesGenerator,
adjectives,
colors,
animals,
} from "unique-names-generator"
import { db } from "../utils/fire-client"
import Layout from "../components/Layout"
import { HOSTNAME } from "../config"
import MDXComponents from "../components/mdx"
export default function Index({ posts, isPage = false, mdxSource }) {
const [mdxCode, setMdxCode] = useState("# THIS IS MDX")
const [message, setMessage] = useState("")
const addPageToCollection = async (name, content) => {
const pagesCollection = collection(db, "pages")
await setDoc(doc(pagesCollection, name), {
name,
content,
})
}
function publishPage() {
const randomName = uniqueNamesGenerator({
dictionaries: [adjectives, colors, animals],
})
addPageToCollection(randomName, mdxCode)
setMessage(
"New Page Added: " + randomName + "\nReload page To see it in the list"
)
setTimeout(() => {
setMessage("")
}, 5000)
}
return (
<Layout>
{isPage ? (
<>
<header>
<nav>
<a href={"http://" + HOSTNAME}>
<a>👈 Go back home</a>
</a>
</nav>
</header>
<main>
<MDXRemote {...mdxSource} components={MDXComponents} />
</main>
</>
) : (
<>
<h1>Home Page</h1>
<textarea
name="mdxCode"
id="mdxCode"
value={mdxCode}
onChange={(e) => setMdxCode(e.target.value)}
className="w-full h-1/2 border-2 border-gray-400 p-2"
/>
<button className="btn btn-primary" onClick={publishPage}>
Publish
</button>
<div>{message}</div>
<ul>
<div className="mt-4 font-bold">Pages List</div>
{posts.map((post) => (
<li key={post.name}>
<a href={`http://${post.name}.${HOSTNAME}`}>{post.name}</a>
</li>
))}
</ul>
</>
)}
</Layout>
)
}
export async function getServerSideProps({ req, res }) {
const host = req.headers.host.split(".")
if (host[0] !== HOSTNAME.split(".")[0] && host[0] !== "www") {
const docRef = doc(db, "pages", host[0])
const docSnap = await getDoc(docRef)
if (docSnap.exists()) {
const { content, data } = matter(docSnap.data().content)
const mdxSource = await serialize(content, {
// Optionally pass remark/rehype plugins
mdxOptions: {
remarkPlugins: [],
rehypePlugins: [],
},
scope: data,
})
if (mdxSource) {
return {
props: {
isPage: true,
mdxSource,
},
}
}
} else {
return {
props: {
redirect: {
destination: "/",
},
},
}
}
}
const pagesCollection = collection(db, "pages")
const pagesSnapshot = await getDocs(pagesCollection)
const pagesList = pagesSnapshot.docs.map((doc) => doc.data())
if (pagesList.length > 0) {
return {
props: {
posts: pagesList,
},
}
}
return { props: { posts } }
}
Update esbuild to 0.13.4 or higher
npm i -D esbuild#0.13.4
See: https://github.com/evanw/esbuild/releases/tag/v0.13.4
If building with Docker this could be due to an incomplete .dockerignore file that doesn't ignore all your node_modules folder.
I am routing dynamically with the compilation of static pages using gatsby-node.
const path = require('path');
const slugify = require('slugify');
const templateForContentType = {
articles: 'src/templates/ArticleDetail.tsx',
};
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions;
.
.
.
result.data.allStrapiFinancialEducationContents.nodes.forEach((node) => {
const contentType = contentTypes[node.content[0].strapi_component];
createPage({
path: `/articles/${slugify(node.title, {
lower: true,
})}`,
component: path.resolve(templateForContentType),
context: {
id: node.id,
},
});
});
};
gatsby compiles that statics files with the .title property
[
{title:"article-seed-0",
{title:"article-seed-1"
]
article-seed-0
...
article-seed-n
when I try to navigate to another page, it forms a url like this:
const {title:"article seed # 0" }
<Link to={article.title}>
this is the result:
Article%20Seed%20#0
the data comes to backend, is there any practical way for any kind of title to a url convention that can be associated with the way gatsby compiles these static files?
Gatsby compiles the static files with the given path:
path: `/articles/${slugify(node.title, {lower: true,})}`,
So, if you are using slugify to convert the title (node.title) to a valid path, it has nothing to do with Gatsby but slugify, which is removing the # to add a hyphen (-) in between among other things (indeed, it's creating a slug from a given string).
If your Link doesn't have the right target URL, just use slugify in the same way:
<Link to={slugify(article.title, {lower:true})}>
I have a directory like this
where I have posts folder which may contain numerous folders and then each video folder may contain some videos.
Now, I have created dynamic pages for the video and video_2 folder using this.
I have two pages now and what I want is to fetch the specific two videos of that directory only in my template page.
what should be my graphQL for this?
Referrence: (For specific content from .md files)
query($slug: String!) {
markdownRemark(fields: { slug: { eq: $slug } }) {
html
frontmatter {
title
}
}
}
`;
This is bringing the specific slug-wise data onto the webpage. What should be the query for sourcing the contents from file-system?(I am using gatsby-source-filesystem).
Hope my question makes sense. Thanks.
So what you're saying is you have a set of directories that should each be turned into a single page. Each page should display the videos that are in the directory.
You could do the following in your gatsby-node; retrieve all files, group them by directory and create pages for each directory.
Note the regex in the query that selects for which files you're interested in.
The createPage call sends the file names videos of the page as context so you might not need to do any further GraphQL query in your page.
See also https://www.gatsbyjs.org/docs/creating-and-modifying-pages/
const path = require("path")
function getDirectoryOfFile(pathName) {
const pathEls = pathName.split(path.sep)
return pathEls[pathEls.length - 2]
}
function groupFilesByDirectory(files) {
return files.reduce((directories, file) => {
const directory = getDirectoryOfFile(file)
directories[directory] = (directories[directory] || []).concat([file])
return directories
}, {})
}
exports.createPages = async function ({ actions, graphql }) {
const { data } = await graphql(`
query {
allFile(filter: { absolutePath: { regex: "/video/" } }) {
edges {
node {
name
absolutePath
}
}
}
}
`)
const files = data.allFile.edges.map(edge => edge.node.absolutePath)
const directories = groupFilesByDirectory(files)
Object.entries(directories).forEach(([directory, videos]) => {
actions.createPage({
path: `/video/${directory}`,
component: require.resolve(/* your page template here */),
context: { videos },
})
})
}
I hope I understood your question right.