I want to implement react-helmet on Meteor. The client side works fine and I can see the tags on the Inspect Element - but how to render it on server side for SEO? I do not understand the documentation.
ReactDOMServer.renderToString(<Handler />);
const helmet = Helmet.renderStatic();
My Code is as follows
index.html (client)
<head>
<body>
<div id="target" />
</body>
</head>
main.js (client)
import React from 'react';
import { Meteor } from 'meteor/meteor';
import { render } from 'react-dom';
import { renderRoutes } from '../imports/startup/client/routes.jsx'
Meteor.startup(() => {
render(renderRoutes(), document.getElementById('target'));
});
I'm using React-Router to render the element to the "target"
main.js (server)
How to get the tags from the server.
import { onPageLoad } from 'meteor/server-render';
Meteor.startup(() => {
onPageLoad((sink) => {
const helmet = Helmet.renderStatic();
sink.appendToHead(helmet.meta.toString());
sink.appendToHead(helmet.title.toString());
});
});
On the about code helmet.meta.toString() returns empty. I know we need to pass something to let the helmet know the meta tags. But how to do that
Can someone help me understand what I need to write on the server to get this working? Everything except that is working fine.
Finally I found the solution :
The code on the main.js (server) should be the following
import ReactDOMServer from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import {renderRoutes} from '../imports/startup/both/routes.jsx'
// ...
onPageLoad((sink) => {
const context = {};
const app = ReactDOMServer.renderToString(
<StaticRouter location={sink.request.url} context={context}>
{renderRoutes()}
</StaticRouter>
);
sink.renderIntoElementById('react-root', app);
const helmet = Helmet.renderStatic();
sink.appendToHead(helmet.meta.toString());
sink.appendToHead(helmet.title.toString());
});
Hope it helps someone who might run into the same issue in the future. :)
Related
In my react app, I am using i18next-http-backend to load translation data from backend response. Currently my app works fine in the below configuration:
config.js
import i18n from 'i18next';
import {initReactI18next} from 'react-i18next';
import HttpApi from 'i18next-http-backend';
import axios from "axios";
const backendOptions = {
loadPath: 'http://localhost:3000/messages',
request: async (options, url, payload, callback) => {
try {
const translation = await axios.get(url);
callback(null,{
status: 200,
data: JSON.stringify(translation.data),
});
} catch (e) {
callback(e);
}
},
};
const i18nextOptions = {
debug:true,
backend: backendOptions,
fallbackLng: 'en',
lng: 'en',
load:"languageOnly",
react: {
useSuspense: true,
},
ns: ['translations'],
defaultNS: 'translations'
}
i18n.use(initReactI18next)
.use(HttpApi )
.init(i18nextOptions);
i18n.languages = ['en', 'jp'];
i18n.on('failedLoading', function(lng, ns, msg) {
console.log("backend error");
})
export default i18n;
index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import './i18n/config';
import {store} from "./app/store";
import {Provider} from "react-redux";
import {ErrorBoundary} from "react-error-boundary";
import {Suspense} from "react";
import RuntimeErrorPage from "./features/error/runtime-error-page";
import Spinner from "./components/common/Spinner";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<ErrorBoundary FallbackComponent={RuntimeErrorPage}>
<Suspense fallback={<Spinner/>}>
<Provider store={store}>
<App/>
</Provider>
</Suspense>
</ErrorBoundary>
</React.StrictMode>
);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
However, if there is any backend error, the app gets stuck in Suspense. If I disable suspense then the i18next initializes without any resource. How can I handle any error comes from request? I need a way to either invoke ErrorBoundary fallback component or redirect to any error page.
i18next never throws an error...
Save those failed loadings somewhere else: https://codesandbox.io/s/react-i18next-http-example-forked-9ted7y?file=/src/i18n.js:188-295
and then create your own logic to show stuff: https://codesandbox.io/s/react-i18next-http-example-forked-9ted7y?file=/src/app.js:155-377
fyi: throwing inside an event listener is not a good idea
btw: why do you want to show an error when translations are not loaded? Why not showing a fallback text? The error is not intended for the end users anyway...
simplifying my post:
my ssr webpage blinks when starting client which means page renders server side rendered html then goes blank and then it starts loading everything all over again.
going through the details:
i'm working on a react project which we decided to change it from being rendered in client to render in server. the project includes react-router-dom ,redux and react-redux ,material-ui which comes with react-jss ,loadable/component ,also handling the head elements by react-helmet-async ,and in ssr it's using express.js which seems to be a must.
for react-router-dom i did everything that is on the docs. using BrowserRouter in client and StaticRouter in ssr and passing a context object to it.
for redux and react-redux i saved preloaded_state in a variable in window and fetched it client side then pass it to store.also fetched some external data to get the content on the source of the page.so i have some requests and data fetching in ssr.
for material-ui i created a new serverSideStyleSheet and collected all the styles from all over the project.
for react-helmet-async i've got different Helmet tags for each page that collects different title ,description and ... individualy.there is also a helmetProvider wrapper for csr and ssr.
at first i used react-helmet but it wasn't compatible with renderToNodeStream.i didn't change react-helmet-async although i'm not using renderToNodeStream but kept it to migrate to renderToNodeStream one day hopefully.
about express.js i'm using it as the docs say but after i added loadable/component i wasn't able to get a successful server side rendering by just adding app.get('*' , ServerSideRender) .so i had to add every url i wanted to render in server in app.get(url ,ServerSideRender).
the other thing about the project is that i didn't eject and created it with create-react-app and there is no webpack config or babelrc but instead i'm using craco.config.js
the last thing is that i've excluded index.html from ssr and instead i've made the tags myself in SSR.js file so index.html gets rendered just in client. and i was so careful about writing tags in ssr exactlly like they are in index.html
solution but not the solution:
this problem is occuring because i use loadable component in my Router.js. so when i import components the normal way there is no blink and everything is fine but unused js decreases my page's perfomance score. i need loadable component stop making the page blink.
diving into the code:
just the client
index.html : rendered just in client
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta name="robots" content="noindex, nofollow" />
<meta data-rh="true" name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no" />
<link href="%PUBLIC_URL%/fonts.css" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
<script src="%PUBLIC_URL%/main.js"></script>
</body>
</html>
index.js : rendered just in client
import React from 'react'
import ReactDOM from 'react-dom'
import {loadableReady} from '#loadable/component'
import App from './App'
import {BrowserRouter} from 'react-router-dom'
import {HelmetProvider} from 'react-helmet-async'
import { Provider } from 'react-redux'
loadableReady(() => {
const preloadedState = window.__PRELOADED_STATE__
delete window.__PRELOADED_STATE__
ReactDOM.hydrate(
<BrowserRouter>
<HelmetProvider>
<Provider store={store(preloadedState)}>
<App />
</Provider>{" "}
</HelmetProvider>
</BrowserRouter>,
document.getElementById("root")
);
})
just the server
ssrIndex.js
require('ignore-styles')
require('#babel/register')({
ignore: [/(node_modules)/],
presets: ['#babel/preset-env', '#babel/preset-react'],
plugins: [
'#babel/plugin-transform-runtime',
'#babel/plugin-proposal-class-properties',
'babel-plugin-dynamic-import-node',
'#babel/plugin-transform-modules-commonjs',
'#loadable/babel-plugin',
],
})
// Import the rest of our application.
require('./SSR.js')
SSR.js
import React from 'react'
import express from 'express'
import ReactDOMServer from 'react-dom/server'
import {StaticRouter} from 'react-router-dom'
import {Provider} from 'react-redux'
import ServerStyleSheets from '#material-ui/styles/ServerStyleSheets'
import {HelmetProvider} from 'react-helmet-async'
import {ChunkExtractor, ChunkExtractorManager} from '#loadable/server'
import path from 'path'
import App from './App'
import store from './redux/store'
import template from './utils/template'
const PORT = 8080
const app = express()
const renderPage = (req, res, preload) => {
const staticRouterContext = {}
const helmetContext = {}
const statsFile = path.resolve(__dirname, '../build', 'loadable-component.json')
const extractor = new ChunkExtractor({statsFile})
const sheets = new ServerStyleSheets()
const html = ReactDOMServer.renderToString(
sheets.collect(
<ChunkExtractorManager extractor={extractor}>
<HelmetProvider context={helmetContext}>
<StaticRouter location={req.url} context={staticRouterContext}>
<Provider store={store(preload)}>
<App />
</Provider>
</StaticRouter>
</HelmetProvider>
</ChunkExtractorManager>,
),
)
const {helmet} = helmetContext
const wholeData = template('scripts', {
chunks: html,
helmet,
extractor,
sheets,
preload,
})
res.send(wholeData)
}
const serverRenderer = (req, res, next) => {
fetchSomeExternalData()
.then(response => {
// response.data is used as preloaded data and passed to the store of redux
// also stored in a variable called __PRELOADED_STATE__ in window to use in client side
// to populate store of redux
renderPage(req, response, response.data)
})
.catch(err => {
// start server side rendering without preloaded data
renderPage(req, res)
})
}
// each url that i want to render on the server side i should add here individually
// which is not so convenient
app.get('/', serverRenderer)
app.get('/my-url-1/', serverRenderer)
app.get('/my-url-2/', serverRenderer)
app.use(express.static(path.join(__dirname, '/../build/')))
// the * doesnt seem to work
app.get('*', serverRenderer)
app.listen(PORT, () => {
if (process.send) {
process.send('ready')
}
})
for both client and server
App.js
<div>
<Header/>
<Router/>
<Footer/>
</div>
i'd be happy to hear any suggestions or solutions. thank you for your time.
I am using Gatsby and need to head a script in the header BEFORE other plugin.
If I add it via gatsby-ssr.js
exports.onRenderBody = ({ setHeadComponents }) => setHeadComponents([#MY-SCRIPT#]);
it's being added last.
How can I add it BEFORE other plugins?
In addition to onRenderBody, use onPreRenderHTML to reorder your scripts.
exports.onRenderBody = ({ setHeadComponents }) =>
setHeadComponents([
<script key='myscript' src='...' />
]);
exports.onPreRenderHTML = ({ getHeadComponents, replaceHeadComponents }) => {
const headComponents = getHeadComponents()
// reorder your array with the sort method, by putting your item at top
const orderedComponents = headComponents.sort((item) => (item.key === 'your-key' ? -1 : 1)); const orderedComponents = reorder(headComponents)
replaceHeadComponents(orderedComponents)
}
For more info, see the Gatsby docs on SSR API.
2022 update
Since the release of the Script Gatsby component (powered by Partytown) it's much easier adding third-party scripts. Just:
import React from "react"
import { Script } from "gatsby"
function YourPage() {
return <Script src="https://my-example-script" />
}
export default YourPage
Why you use gatsby-ssr.js file if you can use <Helmet> tag, maybe it fits you. You just need to use it like this in any component:
import React from "react"
import Helmet from "react-helmet"
import Layout from "../components/layout"
import SEO from "../components/seo"
const IndexPage= () => (
<Layout>
<SEO title="Index page" />
<Helmet>
<script src="https://whatever.com" type="text/javascript"/>
<script src="https://whatever2.com" type="text/javascript"/>
</Helmet>
</Layout>
)
export default IndexPage
The snippet above will load your scripts inside <head> tag on the same order you've placed it.
If you need some kind of ordering and async approach, you can use gatsby-ssr, across onRenderBody and onPreRenderHTML.
I am trying to implement web components using React. The reason I do this, is because we are a big newspaper company and our 3rd party CMS only allows for extendability using web components. So I read about a way to make a react component a web component on the official docs, like so:
const proto = Object.create(HTMLElement.prototype, {
attachedCallback: {
value: function() {
const mountPoint = document.createElement('span');
this.createShadowRoot().appendChild(mountPoint);
const name = this.getAttribute('name');
const url = 'https://www.google.com/search?q=' + encodeURIComponent(name);
ReactDOM.render(<a href={url}>{name}</a>, mountPoint);
}
}
});
document.registerElement('x-search', {prototype: proto});
This actually does work. I created a react app using create-react-app and rewrote the index.js like so:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
// ReactDOM.render(<App />, document.getElementById('root'));
const proto = Object.create(HTMLElement.prototype, {
attachedCallback: {
value: function() {
const mountPoint = document.createElement('span');
this.createShadowRoot().appendChild(mountPoint);
ReactDOM.render(<App />, mountPoint);
}
}
});
document.registerElement('x-my-app', {prototype: proto});
registerServiceWorker();
And yes—this does actually work! However, what this does, is put my CSS in a separate file and as my web component has shadow DOM it is not styled at all. It needs to live inside the generated web component, but how do I do that?
I would like to have 100% coverage on my project.
In order to do so, I need to test my index.js file which is very basic :
index.js
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(<App/>, document.getElementById('root'));
I can't find how to test this.
When creating a function such as :
index.js
const index = (div) => {
ReactDOM.render(<App />, div || document.getElementById('root'));
};
and then testing it :
index.test.js
it('renders without crashing', () => {
const div = document.createElement('div');
index(div);
});
I get an error when importing index:
Invariant Violation: _registerComponent(...): Target container is not a DOM element.
PS:
Note that I already have the following test, working perfectly :
App.test.jsx
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App/>, div);
});
If 100% coverage on your project is your goal and the code in your index.js file is trivial, then it might be a good option to exclude the file from the coverage report, as Andreas Köberle points out in his answer.
Create-react-app currently only supports these four keys in the Jest configuration (source):
collectCoverageFrom
coverageReporters
coverageThreshold
snapshotSerializers
This is why
coveragePathIgnorePatterns": ["src/index.js"]
won't work.
Add following code to the most outer scope of your package.json file:
"jest": {
"collectCoverageFrom": [
"src/**/*.{js,jsx}",
"!src/index.js"
]
}
In the image below you see the output of a test run with this code added to the package.json of the initial app created with create-react-app v1.4.3. Note that the index.js file doesn't show up in the report anymore and also doesn't affect the coverage percentage.
This is how I've tested index.js
index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(<App />, document.getElementById("root"));
index.test.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
jest.mock("react-dom", () => ({ render: jest.fn() }));
describe("Application root", () => {
it("should render without crashing", () => {
const div = document.createElement("div");
div.id = "root";
document.body.appendChild(div);
require("./index.js");
expect(ReactDOM.render).toHaveBeenCalledWith(<App />, div);
});
});
The main question is what you want to test there. If you want to test that your code works correct, write a unit test that spies on ReactDOM.render and mocks document.getElementById('root'). Cause this is all your code does, calling ReactDOM.render with our App component and a specific div.
import ReactDOM from 'react-dom';
...
jest.mock('react-dom', ()=> ({render: jest.fn()}))
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App/>, div);
global.document.getElementById = (id) => id ==='root' && div
expect(ReactDOM.render).toHaveBeenCalledWith(...)
});
If you want test that the app really starts in your page, you should write integration test with Selenium or Nightwatch.js
To just get 100% coverage you can also ignore this file by adding it to the coveragePathIgnorePatterns in your jest settings
I found an article online that explains this way to write the test...
// index.test.js
import Index from './index.js';
it('renders without crashing', () => {
expect(JSON.stringify(
Object.assign({}, Index, { _reactInternalInstance: 'censored' })
)).toMatchSnapshot();
});
Now change the index.js file accordingly:
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
export default ReactDOM.render(
<App />,
document.getElementById('root') || document.createElement('div')
);
Extending on dcastil's answer, here's how to skip these trivial files for a TypeScript project:
Edit package.json
At the root level add the following snippet
{
...rest of existing props,
"jest": {
"collectCoverageFrom": [
"src/**/*.{ts,tsx}",
"!src/serviceWorker.ts",
"!src/index.tsx"
]
}
}
Save and re-run coverage
By now coverage should be higher.
Inspired by the answer above by Shiraz. This is essentially the same solution but for React 17/18. Also it provides some additional test coverage.
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode />,
);
import React from 'react';
import ReactDOM from 'react-dom';
const render= jest.fn().mockName('render');
jest.mock('react');
jest.mock('react-dom', () => ({
createRoot: jest.fn().mockName('createRoot')
}));
let documentSpy=jest.spyOn(document, 'getElementById')
describe('Entry point index test', () => {
const doc =document.createElement('div');
doc.setAttribute('id', 'root');
beforeEach(() => {
ReactDOM.createRoot.mockReturnValue({render});
require("../index.js");
});
it('should call ReactDOM.createRoot once', () => {
expect(ReactDOM.createRoot).toHaveBeenCalledTimes(1);
});
it('should call document.getElementById with root once', () => {
expect(documentSpy).toHaveBeenCalledTimes(1);
expect(documentSpy).toHaveBeenCalledWith('root');
});
it('should call render with React.StrictMode', () => {
expect(render).toHaveBeenCalledTimes(1);
expect(render).toHaveBeenCalledWith( <React.StrictMode />,);
});
});
Here what i did and looks like it works just perfect (100% coverage, app doesn't break):
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css';
export const ReactStrictMode = <React.StrictMode>
<App />
</React.StrictMode>
export const rootElement = document.getElementById('root')
ReactDOM.render(
ReactStrictMode,
rootElement
);
and then in index.spec.js:
// src/index.spec.js
/* eslint-env jest */
import React from 'react'
import ReactDOM from 'react-dom'
import { ReactStrictMode, rootElement } from './index'
jest.mock('react-dom', () => ({ render: jest.fn() }))
describe('index.js', () => {
it('renders without crashing', () => {
ReactDOM.render(ReactStrictMode, rootElement)
expect(ReactDOM.render).toHaveBeenCalledWith(ReactStrictMode, rootElement)
})
})
Added a couple of more test cases. Any feedback would be appreciated...
import React from "react";
import { render, cleanup } from "#testing-library/react";
import ReactDOM from "react-dom";
import App from "./App";
afterEach(cleanup);
// jest.mock will mock all the function using jest.fn() that is present inside the react-dom library
jest.mock("react-dom");
describe("Testing Application Root", () => {
it("should render without crashing", () => {
const div = document.createElement("div");
div.id = "root";
document.body.appendChild(div);
require("./index");
expect(ReactDOM.render).toHaveBeenCalledWith(<App />, div);
});
it("should render the app inside div which has root id", () => {
expect(global.document.getElementById("root")).toBeDefined();
});
it("should render App component", () => {
expect(App).toBeDefined();
});
});