I am setting up a Progressive Web App supporting offline browsing.
I have already set up offline browsing for my main route ('domainsample.com/') and it responds 200 even if offline.
But when I navigate to other routes ('domainsample.com/about') I receive a No Internet Page error.
Here is a sample I deployed in Heroku the URL: https://pwa-hehe.herokuapp.com
I used Vue CLI 3 to set up the project and Node.js and Express.js to run my dist folder in the server.
// server.js
const express = require('express')
const path = require('path')
const history = require('connect-history-api-fallback')
const app = express()
const staticFileMiddleware = express.static(path.join(__dirname + '/dist'))
app.use(staticFileMiddleware)
app.use(history({
disableDotRule: true,
verbose: true
}))
app.use(staticFileMiddleware)
app.get('/', function (req, res) {
res.render(path.join(__dirname + '/dist/'))
})
var server = app.listen(process.env.PORT || 3000, function () {
var port = server.address().port
console.log("App now running on port", port)
})
// manifest.json
{
"name": "pwa-offline",
"short_name": "pwa-offline",
"icons": [
{
"src": "./img/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "./img/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "./index.html",
"display": "standalone",
"background_color": "#000000",
"theme_color": "#4DBA87"
}
// service-worker.js
/**
* Welcome to your Workbox-powered service worker!
*
* You'll need to register this file in your web app and you should
* disable HTTP caching for this file too.
*
*
* The rest of the code is auto-generated. Please don't update this file
* directly; instead, make changes to your Workbox build configuration
* and re-run your build process.
*
*/
importScripts("https://storage.googleapis.com/workbox-cdn/releases/3.6.3/workbox-sw.js");
importScripts(
"/precache-manifest.d3f1ce5d8331bddc555348f44cfba9d8.js"
);
workbox.core.setCacheNameDetails({prefix: "pwa-offline"});
/**
* The workboxSW.precacheAndRoute() method efficiently caches and responds to
* requests for URLs in the manifest.
*
*/
self.__precacheManifest = [].concat(self.__precacheManifest || []);
workbox.precaching.suppressWarnings();
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});
There's a simpler solution that doesn't involve using InjectManifest.
Simply add this to your vue.config.js file:
pwa: {
workboxOptions: {
navigateFallback: 'index.html'
}
}
It will automatically add the necessary code to your service worker:
workbox.routing.registerNavigationRoute(workbox.precaching.getCacheKeyForURL("index.html"));
I had the same problem and I found the solution here: https://github.com/vuejs/vue-cli/issues/717#issuecomment-382079361
To anyone who is also using the vue/cli-pligin-pwa and vue-cli 3, I have solved this problem.
The vue/cli-pligin-pwa does its job in caching your js files for offline use of your web app.
but since you're using a Single Page App, you'll have to set a some kind of fallback.
The request for the page e.g.(sample.com/about) will be a navigation request and it will serve the cached page for /index.html
Source: https://developers.google.com/web/tools/workbox/modules/workbox-routing#how_to_register_a_navigation_route
So what I did is, I made a custom service worker by entering this line of code in my
vue.config.js file
// vue.config.js
module.exports = {
pwa: {
// configure the workbox plugin
workboxPluginMode: 'InjectManifest',
workboxOptions: {
swSrc: 'public/service-worker.js'
}
}
}
and now, create a service-worker.js in your public folder directory, and write up the the generated line of codes from vue/cli-pligin-pwa and add the line :
workbox.routing.registerNavigationRoute('/index.html');
//service-worker.js
self.__precacheManifest = [].concat(self.__precacheManifest || []);
workbox.precaching.suppressWarnings();
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});
workbox.routing.registerNavigationRoute('/index.html');
// install new service worker when ok, then reload page.
self.addEventListener("message", msg => {
if (msg.data.action == 'skipWaiting') {
self.skipWaiting()
}
})
The problem is that your service worker is not aware of which assets to cache. What is the value of self.__precacheManifest? (probably the web manifest file).
You would need to configure workbox in order to have some assets for the precacheAndRoute method, as this will instruct the service worker on which files to cache and when. Something like:
workbox.precaching.precacheAndRoute([
'/styles/example.ac29.css',
'/app/offlinePage.js',
'/app/offlinePage.html',
// ... other entries ...
]);
Per default a service worker does not know which assets or HTTP responses to cache, therefore we have to define and implement a caching strategy according to our requirements.
I wrote an article about service workers and the available caching strategies. Have a look at it if you want to deepen the topic.
Related
I created a landing page using Astro with Tailwind CSS. And it is currently hosted on Vercel. I wanted to try out the analytics service provided by Vercel. I have been able to avail the Audience analytics service provided by Vercel. However, I cannot avail the web vitals services. After enabling the service and redeploying my project, I am stuck in this screen (screen shot provided).
Please note that I did turn off the ad blocker but that did not resolve the issue.I also added the following meta tag to resolve any CSP issue
<meta http-equiv="Content-Security-Policy"
content="default-src 'self' vitals.vercel-insights.com"/>
But that has not solved the problem.That is why I want to know does Vercel support analytics for Astro projects and if they do, then what am I doing wrong? Thank you.
Vercel’s Web Vitals analytics currently only has out-of-the-box support for Next, Nuxt, and Gatsby.
To track Web Vitals with a different framework like Astro, you need a bit of manual set up work as documented in Vercel’s Web Vitals API docs.
For example in your base Astro layout you could include a script tag that will import their example code and run it:
---
// src/layouts/BaseLayout.astro
---
<script>
import { webVitals } from '../scripts/vitals';
const analyticsId = import.meta.env.PUBLIC_VERCEL_ANALYTICS_ID;
webVitals({
path: window.location.pathname,
analyticsId,
});
</script>
Here’s Vercel’s example vitals.js snippet:
// src/scripts/vitals.js
import { getCLS, getFCP, getFID, getLCP, getTTFB } from 'web-vitals';
const vitalsUrl = 'https://vitals.vercel-analytics.com/v1/vitals';
function getConnectionSpeed() {
return 'connection' in navigator &&
navigator['connection'] &&
'effectiveType' in navigator['connection']
? navigator['connection']['effectiveType']
: '';
}
function sendToAnalytics(metric, options) {
const body = {
dsn: options.analyticsId, // qPgJqYH9LQX5o31Ormk8iWhCxZO
id: metric.id, // v2-1653884975443-1839479248192
page: options.path, // /blog/my-test
href: location.href, // https://my-app.vercel.app/blog/my-test
event_name: metric.name, // TTFB
value: metric.value.toString(), // 60.20000000298023
speed: getConnectionSpeed(), // 4g
};
if (options.debug) {
console.log('[Analytics]', metric.name, JSON.stringify(body, null, 2));
}
const blob = new Blob([new URLSearchParams(body).toString()], {
// This content type is necessary for `sendBeacon`
type: 'application/x-www-form-urlencoded',
});
if (navigator.sendBeacon) {
navigator.sendBeacon(vitalsUrl, blob);
} else
fetch(vitalsUrl, {
body: blob,
method: 'POST',
credentials: 'omit',
keepalive: true,
});
}
export function webVitals(options) {
try {
getFID((metric) => sendToAnalytics(metric, options));
getTTFB((metric) => sendToAnalytics(metric, options));
getLCP((metric) => sendToAnalytics(metric, options));
getCLS((metric) => sendToAnalytics(metric, options));
getFCP((metric) => sendToAnalytics(metric, options));
} catch (err) {
console.error('[Analytics]', err);
}
}
For a slightly more real-world implementation you, check out the <TrackVitals> Astro component in the astro-badge repo.
Vercel analytics has support for frameworks other than Next, Nuxt Gatsby etc. The way to achieve it in Astro (1.6, 2.0 etc.) is to install the #vercel/analytics package and inject a simple <script> tag that imports it and calls its exported function inject():
<script>
import { inject } from '#vercel/analytics'
// #ts-ignore: process.env.NODE_ENV is required by #vercel/analytics internally
// so that it can determine the correct path for importing the analytics script
globalThis.process = { env: { NODE_ENV: import.meta.env.MODE } }
inject()
</script>
You can inject this code in your <head> section in any .astro template file.
Unfortunately, the package is expecting a non-ESM runtime environment and is internally conditionally checking for process.env.NODE_ENV to determine which script to load (local-relative path to JS or from a remote host, fully qualified domain name). This is the reason, the MODE needs to be exposed as process.env.NODE_ENV. I tried to achieve this via Vite using define, but Astro seems to check for process somewhere else internally and fails.
I thought this would be a simple task, but I struggle to find a way to force my webpage to use https.
The next.js webapp lives on the heroku servers and I've set up the SSL. Both https and http version works, but how to I force or redirect the website to use the https version.
I've seen some solution using express, but nothing in my webapp are using express, is it required?
Thanks.
As of Nextjs v12, you can use middleware instead of a setting up a custom server.
Middleware is a better solution for the following reasons:
a custom server often requires additional dependencies (like express)
you give up some of the box features like automatic static optimization
Middleware can be scope to specific paths using the built in routing paradigm
Create a /pages/_middleware.ts (or .js) file with something similar to this:
import { NextFetchEvent, NextRequest, NextResponse } from 'next/server'
type Environment = "production" | "development" | "other";
export function middleware(req: NextRequest, ev: NextFetchEvent) {
const currentEnv = process.env.NODE_ENV as Environment;
if (currentEnv === 'production' &&
req.headers.get("x-forwarded-proto") !== "https") {
return NextResponse.redirect(
`https://${req.headers.get('host')}${req.nextUrl.pathname}`,
301
);
}
return NextResponse.next();
}
I also created an npm package for this.
import sslRedirect from 'next-ssl-redirect-middleware';
export default sslRedirect({});
There is a solution with an NPM library called heroku-ssl-redirect.
First off, install the library using npm i heroku-ssl-redirect.
Then, create a new server.js file with the following script.
const next = require('next');
const express = require('express');
const sslRedirect = require('heroku-ssl-redirect').default; // to make it work with 'require' keyword.
const PORT = process.env.PORT || 3000;
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
app.prepare().then(() => {
const server = express();
// Express's middleware to automatically redirect to 'https'.
server.use(sslRedirect());
server.all('*', (req, res) => {
return handle(req, res);
});
server.listen(port, err => {
if (err) throw err;
console.log(`Server starts on ${PORT}.`);
});
});
Then, change the start script to be like this:
"scripts": {
"dev": "next",
"build": "next build",
"start": "node server.js"
}
It should work.
Note that you could replace Express with the createServer method from native Node.js's http module. But I do this with Express to simplify the syntax.
Further reading: How to setup a custom server in Next.js.
following https://nuxtjs.org/api/configuration-env
I have been trying to set up my apiUrl in nuxt.config.js once for the whole project, like:
export default {
env: {
apiUrl: process.env.MY_REMOTE_CMS_API_URL || 'http://localhost:1337'
}
}
adding this in nuxt.config.js, I'd expect (and would like) to have apiUrl accessible everywhere in the project.
In particular, it is needed for the 3 following cases:
with axios, to generate static pages from dynamic urls (in nuxt.config.js)
generate: {
routes: function () {
return axios.get(apiUrl + '/posts')
.then((res) => {
return res.data.filter(page => {
return page.publish === true;
}).map(page => {
return {
route: '/news/' + page.slug
}
})
})
}
},
with apollo, to get data via graphql (in nuxt.config.js)
apollo: {
clientConfigs: {
default: {
httpEndpoint: apiUrl + '/graphql'
}
}
},
in every layout, page and components, as the base url of media:
<img :src="apiUrl + item.image.url" />
As you might see, only thing I need is to 'print' the actual base url of the cms.
I have also tried to access it with process.env.apiUrl, with no success.
The only way I was able to make it has been to create an extra plugin/apiUrl.js file, which injects the api url, and seems wrong to me as I am now setting the apiUrl twice in my project.
I asked this question in the past, but in a way less clear way. I was suggested to use dotenv, but from the docs it looks like adding an additional layer of complication that might not be necessary for a simpler setup.
Thanks.
I think dotenv module really is what you need.
This is my setup:
Project root has a .env file that contains
BASE_URL=https://www.myapi.com
require('dotenv').config() at top of nuxt.config.js
#nuxtjs/dotenv installed and added to buildModules of nuxt.config.js
env: { BASE_URL: process.env.BASE_URL} added to nuxt.config.js
axios: { baseURL: process.env.BASE_URL } added to nuxt.config.js (optional)
You should have access to your .env throughout the project. (process.env.BASE_URL)
I haven't used apollo, but you should be able to set the apollo endpoint with process.env.BASE_URL + '/graphql'
As of Nuxt 2.13, #nuxtjs/dotenv is not required anymore. Read here
The concept that I was missing is that you set up the same named variable in your server / pipeline, so that you have your (always local / never pushed) .env file and a same name variable remotely, not added to your repo (where the value can be the same or different)
I'm having exactly the same issue reported at https://github.com/GoogleChrome/workbox/issues/1663 which describes an issue that occurs exclusively in Safari where mp4 videos are not rendered after being cached by the service worker.
I'm using workbox-webpack-plugin, so the instructions provided in the comment https://github.com/GoogleChrome/workbox/issues/1663#issuecomment-448755945 will not work in my case. I'm not being able to require workbox-range-requests plugin in my webpack config file and pass it to the runtime caching options because I believe this package is intended for browser usage only. My workbox config is precaching .mp4 assets and uses a network first strategy for runtime caching.
How can I setup workbox-range-requests with workbox-webpack-plugin?
EDIT: Following Jeff's answer below, I've adjusted my webpack config to the following:
new WorkboxPlugin.InjectManifest({
swSrc: serviceWorkerSrcPath,
swDest: serviceWorkerBuildPath,
importsDirectory: 'sw',
})
The build produces the following service worker:
importScripts("/_build/sw/precache-manifest.8a0be820b796b153c97ba206d9753bdb.js", "https://storage.googleapis.com/workbox-cdn/releases/3.6.2/workbox-sw.js");
workbox.precaching.precacheAndRoute(self.__precacheManifest || []);
workbox.routing.registerRoute(
/.*\.mp4/,
new workbox.strategies.CacheFirst({
cacheName: 'videos',
plugins: [
new workbox.cacheableResponse.Plugin({ statuses: [200] }),
new workbox.rangeRequests.Plugin(),
],
}),
);
If forgot to mention previously, but I've also added crossOrigin="anonymous" attribute to the video elements.
EDIT:
Repro that demonstrates it does not work as expected on Safari: https://github.com/acostalima/workbox-range-requests-mp4-demo
There's specific guidance for this use case in the "Serve cached audio and video" recipe in the Workbox documentation.
You can continue using the workbox-webpack-plugin, but I'd suggest using it in InjectManifest mode, which will give you control over the top-level service worker file. That will in turn make it possible to follow the recipe.
This documentation has guidance on configuring workbox-webpack-plugin in InjectManifest mode.
I had the same issue with Safari and managed to resolve it by removing my video from the precahe list self.__precacheManifest and instead by adding it in the service worker's install handler:
self.addEventListener('install', (event) => {
const urls = [/* videoUrl */];
const cacheName = 'videos';
event.waitUntil(caches.open(cacheName).then((cache) => cache.addAll(urls)));
});
Looking at the logs, it seemed that otherwise only the precache was used to respond to the request for the video resource and not the router.
Although the docs say that adding mp4s to the precache cache and then configuring the range plugin to handle precache mp4s is supposed to work, in practice, it wasn't. Removing mp4s from the precache and configuring your own video cache with the range plugin did the trick for me. Don't forget to add the crossorigin="anonymous" tag to your videos!
Here's how I did it (webpack 5, workbox 6):
// src/service-worker.js
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
import { cacheNames } from 'workbox-core';
import { precacheAndRoute } from 'workbox-precaching';
import { RangeRequestsPlugin } from 'workbox-range-requests';
import { registerRoute } from 'workbox-routing';
import { CacheFirst } from 'workbox-strategies';
const allEntries = self.__WB_MANIFEST; // Injected by WorkboxWebpackPlugin at compile time
const videoEntries = allEntries.filter((entry) => entry.url.endsWith('.mp4'));
const restEntries = allEntries.filter((entry) => !entry.url.endsWith('.mp4'));
precacheAndRoute(restEntries);
const videoCacheName = `${cacheNames.prefix}-videos-${cacheNames.suffix}`;
self.addEventListener('install', (event) => {
const allVideosAddedToCache = caches.open(videoCacheName).then((videoCache) => {
const videoUrls = videoEntries.map((entry) => entry.url);
return videoCache.addAll(videoUrls);
});
event.waitUntil(allVideosAddedToCache);
});
registerRoute(
(route) => route.url.pathname.endsWith('.mp4'),
new CacheFirst({
cacheName: videoCacheName,
plugins: [new CacheableResponsePlugin({ statuses: [200] }), new RangeRequestsPlugin()],
})
);
// webpack.config.js
plugins: [
new WorkboxWebpackPlugin.InjectManifest({
swSrc: 'src/service-worker.js',
}),
]
// index.tsx
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js');
});
}
I have my sails app set to serve static files from a custom directory
module.exports.http = {
customMiddleware: function(app) {
app.use('/', express.static(process.cwd() + '/public'));
},
middleware: {
xframe: require('lusca').xframe('SAMEORIGIN'),
order: [
'xframe',
'startRequestTimer',
'cookieParser',
'customMiddleware',
'session',
'myRequestLogger',
'bodyParser',
'handleBodyParserError',
'compress',
'methodOverride',
'poweredBy',
'$custom',
'router',
'www',
'favicon',
'404',
'500'
]
}
};
and my frontend app get served using the route
'/*': {
view: 'index',
skipAssets: true,
skipRegex: /(^\/api\/.*$)|^\/csrfToken$/
}
but I've noticed that if I request a file that doesn't exist i.e /css/whataloadofrubbish.css it just hangs when it should send a 404. Not sure what I'm missing.
Ah compared to another app and found my notFound.js find was wrong