I'm writing a test with AVA and jsdom.
This works:
const rp = require('request-promise-native')
const jsdom = require('jsdom')
const {JSDOM} = jsdom
const url = 'http://localhost:8000'
rp(url).then((body) => {
const options = {
url: url,
resources: 'usable',
runScripts: 'dangerously',
}
let dom = new JSDOM(body, options)
dom.window.document.addEventListener('DOMContentLoaded', () => {
setImmediate(() => {
console.log(dom.serialize()) // works, prints <html><head></head><body><h1>Hello world!</h1></body></html>
})
})
})
An external script adds an h1 to the document body.
// external.js
document.addEventListener('DOMContentLoaded', () => {
document.write('<h1>Hello world!</h1>')
})
This is the markup at http://localhost:8000:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JSDOM test</title>
<script src="external.js"></script>
</head>
<body>
</body>
</html>
When I try the same thing in a test:
const rp = require('request-promise-native')
const test = require('ava')
const jsdom = require('jsdom')
const {JSDOM} = jsdom
const url = 'http://localhost:8000'
test('Check custom CSS', t => {
return rp(url).then((body) => {
const options = {
url: url,
resources: 'usable',
runScripts: 'dangerously',
}
let dom = new JSDOM(body, options)
dom.window.document.addEventListener('DOMContentLoaded', () => {
setImmediate(() => {
console.log(dom.serialize()) // nothing gets printed
})
})
t.pass('passed')
})
})
console.log isn't called. I would expect <html><head></head><body><h1>Hello world!</h1></body></html> to get printed, rather than there being no output.
In summary, I want to:
Grab the response body from a url
Create a new jsdom object from it
Execute the scripts linked to in the response body's head tags
Let the scripts change the markup
Print the resulting modified markup
What is the best way of going about doing this?
Here's a minimum working example: https://github.com/cg433n/jsdom-test
Related
I am writing a test to check if it loads script correctly. This script is to load scripts for user tracking.
Here is my pseudo code.
import Head from 'next/head';
export const TrackingScript = () => {
return (
<Head>
<script
type="text/javascript"
src="https://..."
</script>
</Head>
)
}
I want to write a test
import { render } from '#testing-library/react';
import { TrackingScript } from './TrackingScript';
it('loads tracking script correctly', () => {
render(TrackingScript);
...
})
I am trying to create a dynamic page component [page].js that can display pages dynamically. The route is set dynamically to
localhost:3000/[channel]/[store]/page/[slug]
I provided slug as params in paths array in getStaticPaths but I'm getting the following error:
Server Error
Error: A required parameter (channel) was not provided as a string in getStaticPaths for /[channel]/[store]/page/[slug]
This error happened while generating the page. Any console logs will be displayed in the terminal window.
It looks like it's asking for channel and store params?
I do have channel and store data in getStaticProps but not in getStaticPaths.
I've set up getStaticPaths with slug param but I'm not sure how to find out channel and store params here.
import {fetchLayout} from "../../../../http/server/fetchLayout";
import Layout from "../../../../views/layout/Layout/Layout";
import Head from "../../../../resources/components/Head/Head";
import {fetchPage} from "../../../../http/server/fetchPage";
import {fetchPages} from "../../../../http/server/fetchPages";
const Page = ({ layout, page }) =>
{
if ( page && layout ) {
const { title, meta_title, description, keywords } = page
return (
<Layout data={ layout }>
<Head title={ meta_title } >
<meta name="description" content={ description } />
<meta name="keywords" content={ keywords } />
</Head>
<h1 style={{ textAlign: `center`, margin: `50px 0` }}>{ title }</h1>
</Layout>
)
}
return null
}
export const getStaticProps = async ({ params }) =>
{
const layout = await fetchLayout()
// const { channel, store } = layout.region
const page = await fetchPage( params.slug )
return { props: { layout, page }}
}
export const getStaticPaths = async () =>
{
const pages = await fetchPages()
const paths = pages.map(({ slug }) => ({ params: { slug }}))
return { paths, fallback: true }
}
export default Page
I am a bit confused by the documentation and not sure if it's possible what I am trying to do.
Goal:
Export NextJS app statically and host it on netlify
Allow users to create posts and have links to these posts which work
For example:
User creates a new post with the id: 2
This post should be publicly accessible under mysite.com/posts/2
I'd imagine that I can create a skeleton html file called posts.html and netlify redirects all posts/<id> requests to that posts.html file which will then show the skeleton and load the necessary data via an API on the fly.
I think without this netlify hack, my goal of static export + working links to dynamic routes is not possible with next.js according to their documentation since fallback: true is only possible when using SSR.
Question: How can I achieve my dream setup of static nextjs export + working links to dynamic routes?
EDIT:
I just found out about Redirects. They could be the solution to my problem.
getStaticProps and getStaticPaths()
It looks like using getStaticProps and getStaticPaths() is the way to go.
I have something like this in my [post].js file:
const Post = ({ pageContent }) => {
// ...
}
export default Post;
export async function getStaticProps({ params: { post } }) {
const [pageContent] = await Promise.all([getBlogPostContent(post)]);
return { props: { pageContent } };
}
export async function getStaticPaths() {
const [posts] = await Promise.all([getAllBlogPostEntries()]);
const paths = posts.entries.map((c) => {
return { params: { post: c.route } }; // Route is something like "this-is-my-post"
});
return {
paths,
fallback: false,
};
}
In my case, I query Contentful using my getAllBlogPostEntries for the blog entries. That creates the files, something like this-is-my-post.html. getBlogPostContent(post) will grab the content for the specific file.
export async function getAllBlogPostEntries() {
const posts = await client.getEntries({
content_type: 'blogPost',
order: 'fields.date',
});
return posts;
}
export async function getBlogPostContent(route) {
const post = await client.getEntries({
content_type: 'blogPost',
'fields.route': route,
});
return post;
}
When I do an npm run export it creates a file for each blog post...
info - Collecting page data ...[
{
params: { post: 'my-first-post' }
},
{
params: { post: 'another-post' }
},
In your case the route would just be 1, 2, 3, etc.
Outdated Method - Run a Query in next.config.js
If you are looking to create a static site you would need to query the posts ahead of time, before the next export.
Here is an example using Contentful which you might have set up with blog posts:
First create a page under pages/blog/[post].js.
Next can use an exportMap inside next.config.js.
// next.config.js
const contentful = require('contentful');
// Connects to Contentful
const contentfulClient = async () => {
const client = await contentful.createClient({
space: process.env.NEXT_PUBLIC_CONTENTFUL_SPACE_ID,
accessToken: process.env.NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN,
});
return client;
};
// Gets all of the blog posts
const getBlogPostEntries = async (client) => {
const entries = await client.getEntries({
content_type: 'blogPost',
order: 'fields.date',
});
return entries;
};
module.exports = {
async exportPathMap() {
const routes = {
'/': { page: '/' }, // Index page
'/blog/index': { page: '/blog' }, // Blog page
};
const client = await contentfulClient();
const posts = await getBlogPostEntries(client);
// See explanation below
posts.items.forEach((item) => {
routes[`/blog/${item.fields.route}`] = { page: '/blog/[post]' };
});
return routes;
},
};
Just above return routes; I'm connecting to Contentful, and grabbing all of the blog posts. In this case each post has a value I've defined called route. I've given every piece of content a route value, something like this-is-my-first-post and just-started-blogging. In the end, the route object looks something like this:
routes = {
'/': { page: '/' }, // Index page
'/blog/index': { page: '/blog' }, // Blog page
'/blog/this-is-my-first-post': { page: '/blog/[post]' },
'/blog/just-started-blogging': { page: '/blog/[post]' },
};
Your export in the out/ directory will be:
out/
/index.html
/blog/index.html
/blog/this-is-my-first-post.html
/blog/just-started-blogging.html
In your case, if you are using post id numbers, you would have to fetch the blog posts and do something like:
const posts = await getAllPosts();
posts.forEach((post) => {
routes[`/blog/${post.id}`] = { page: '/blog/[post]' };
});
// Routes end up like
// routes = {
// '/': { page: '/' }, // Index page
// '/blog/index': { page: '/blog' }, // Blog page
// '/blog/1': { page: '/blog/[post]' },
// '/blog/2': { page: '/blog/[post]' },
// };
The next step would be to create some sort of hook on Netlify to trigger a static site build when the user creates content.
Also here is and idea of what your pages/blog/[post].js would look like.
import Head from 'next/head';
export async function getBlogPostContent(route) {
const post = await client.getEntries({
content_type: 'blogPost',
'fields.route': route,
});
return post;
}
const Post = (props) => {
const { title, content } = props;
return (
<>
<Head>
<title>{title}</title>
</Head>
{content}
</>
);
};
Post.getInitialProps = async ({ asPath }) => {
// asPath is something like `/blog/this-is-my-first-post`
const pageContent = await getBlogPostContent(asPath.replace('/blog/', ''));
const { items } = pageContent;
const { title, content } = items[0].fields;
return { title, content };
};
export default Post;
I'm working on SEO component which needs a canonical URL.
How can I get URL of static page in Next.js with Automatic Static Optimization turned on?
Using useRouter from next/router you can get the pathname for the current page and use it in a <Head/> tag as following:
import { useRouter } from "next/router";
const site = "https://gourav.io";
const canonicalURL = site + useRouter().pathname;
<Head>
<link rel="canonical" href={canonicalURL} />
</Head>
Building on fzembow's comment about useRouter().asPath and GorvGoyl's answer, here's an implementation which manages to handle both dynamic routes and excludes anchor and query param URL extensions:
import { useRouter } from "next/router";
const CANONICAL_DOMAIN = 'https://yoursite.com';
const router = useRouter();
const _pathSliceLength = Math.min.apply(Math, [
router.asPath.indexOf('?') > 0 ? router.asPath.indexOf('?') : router.asPath.length,
router.asPath.indexOf('#') > 0 ? router.asPath.indexOf('#') : router.asPath.length
]);
const canonicalURL= CANONICAL_DOMAIN + router.asPath.substring(0, _pathSliceLength);
<Head>
<link rel="canonical" href={ canonicalURL } />
</Head>
use package called next-absolute-url . It works in getServerSideProps. Since getStaticProps run on build time so dont have data available.
can be used as
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const { req, query } = ctx;
const { origin } = absoluteUrl(req);
return {
props: {
canonicalUrl: `${origin}/user-listings`,
},
};
};
export const getStaticProps:GetStaticProps = async (ctx) => {
return {
props: {
canonicalUrl: 'https://www.test.com',
},
};
};
A super ugly, yet, the most adequate solution I found:
const canonicalUrl = typeof window === 'undefined' ?
'' :
`${window.location.origin}/${window.location.pathname}`;
My solution was to use new URL with router.asPath.
const CURRENT_URL = process.env.NEXT_PUBLIC_CURRENT_SITE_URL
const getCanonical = (path: string) => {
const fullURL = new URL(path, CURRENT_URL)
return`${fullURL.origin}${fullURL.pathname}
}
export const Page = () => {
const router = useRouter()
const canonical = getCanonical(router.asPath)
<Head>
<link rel="canonical" href={canonical} />
</Head>
<div>
...
<div>
}
I added a canonical link tag to _app.js so that it appears on every page:
<link
rel="canonical"
href={typeof window !== 'undefined' && `${window.location.origin}${useRouter().asPath}`}
key="canonical"
/>
Have problem in my express app. Have some ejs templates, where css style file applied correctly. For example "localhost***/page", but when i go to "localhost***/page/secondpage" my ejs doesn't see style even if i apply it.
A little bit of code
app.js
app.use(express.static(path.join(__dirname, 'public')));
//some code
app.use('/', require('./routes/index'));
app.use('/events', require('./routes/events'));
some routes
const express = require('express');
const router = express.Router();
router.get('/', function (req, res, next) {
let query = *db query*;
res.locals.connection.query(query, function (error, results, fields) {
if (error) throw error;
res.render('index', { title: 'Events', data: results });
});
});
router.get('/:id', function (req, res) {
let query = *db query*;
res.locals.connection.query(query, function (error, results, fields) {
if (error) throw error;
res.render('singleevent', { title: 'Event', data: results });
});
});
Head of localhost***/events page and of next page localhost***/events/singleevent
<head>
<meta charset="UTF-8">
<title>
<%= title %>|Minsk Events</title>
<link rel="stylesheet" href="css/style.css">
</head>
There are my dirs
You asked for css file related to the html file.
So if html file is placed under events, then css file should be at:
/events/css/style.css
etc.