I'm interested in strategies to add apple-app-site-association to a Gatsby site. more here
Apple requires that it be at https://domain.com/.well-known/apple-app-site-association and return flat json. (no html!)
In m production website, I'm using Cloudfront in front of s3, so linking directly to it does not work. The Gatsby router picks up the route and gives me a 404.
Thank you!
@jeffchuber wouldn't you need to add this to the static folder? https://www.gatsbyjs.org/docs/static-folder/
This will be copied as-is to the public folder, which can then be deployed to e.g. S3 very easily.
Also: this seems like this could be a good idea for a plugin to create this automatically!
Going to close as answered, but please feel free to re-open/reply if we can help further!
@DSchau thanks for the quick reply!
The issue is not getting a file into the static folder, the issue is accessing that file by the route/url apple demands https://domain.com/.well-known/apple-app-site-association.
Desired scenario:
http://localhost:8000/.well-known/apple-app-site-association{
"webcredentials": {
"apps": [
"KAW43335BG.com.companyName.App1",
"KAW43335BG.com.companyName.App2"
]
}
}
Looks like this is not an uncommon issue: https://github.com/gatsbyjs/gatsby/issues/4144
@jeffchuber you need to set up a redirect for your provider. For example, with S3: https://docs.aws.amazon.com/AmazonS3/latest/dev/how-to-page-redirect.html
I've set up an example with Netlify with this repo, and you can see it working:
https://gatsby-apple-app-site-association.netlify.com/.well-known/apple-app-site-association
@DSchau I really appreciate the reply! I do think this is something a lot of devs will encounter or I wouldn't spend the time.
Unfortunately it needs to be solved at the routing level because Apple rejects redirects:

https://developer.apple.com/documentation/security/password_autofill/setting_up_an_app_s_associated_domains
My understanding is that enabling a dev to add a route to gatsby-node.js that can point directly at a file (instead of a React component) would be ideal. Something like this perhaps.
createPage({
path: '/.well-known/apple-app-site-association',
file: path.resolve('./public/.well-known/apple-app-site-association.json'),
})
Ah! Come on Apple, not making it easy on us!
Let's re-open this and find a more robust solution.
@DSchau what do you think about the idea above of setting up a route to serve a static file?
Hi @jeffchuber,
Why do you need to route to the apple-app-site-association file? Safari and your iOS app look for if the file exists on your server for credential sharing.
It may be that your server is restricting access to dotfiles which are hidden to some OSs.
I use express to serve the gatsby files and express.static to allow access to the .well-known folder.
app.use(
express.static(serverSettings.root, {
dotfiles: "allow"
})
);
app.get("/.well-known/change-password", (req, res) => {
res.redirect(301, "/change-password");
});

@rafcontreras thanks for the idea!
currently im using Cloudfront -> S3 -> gatsby static site.
I found a workaround which is hosting on a subdomain, so I'm going to close this for now out of respect for the community.
Hi @jeffchuber,
Why do you need to route to the
apple-app-site-associationfile? Safari and your iOS app look for if the file exists on your server for credential sharing.It may be that your server is restricting access to dotfiles which are hidden to some OSs.
I use express to serve the gatsby files and express.static to allow access to the
.well-knownfolder.app.use( express.static(serverSettings.root, { dotfiles: "allow" }) ); app.get("/.well-known/change-password", (req, res) => { res.redirect(301, "/change-password"); });
Where do you use express exactly ?
here are the 2 ressource I found to serve this .well-know folder
https://www.gatsbyjs.org/docs/api-proxy/
https://www.gatsbyjs.org/packages/gatsby-plugin-express/
second one also dont give info of where the express app should be called
Hey @adberard
I used Gatsby to create a hybrid app, so I need a server in between the user and the different APIs as some have dumb CORS settings.
I'm using expressJS to proxy API requests and I'm hosting it in Azure.
Here's my very small express server server.js:
const url = require("url");
const express = require("express");
const gatsyExpress = require("gatsby-plugin-express");
const proxy = require("express-http-proxy");
const bodyParser = require("body-parser");
const expressStaticGzip = require("express-static-gzip");
// NodeJS Express server
const app = express();
// In case the server is behind a proxy
app.enable("trust proxy");
/* Removes the X-Powered-By header
to make it slightly harder for attackers
to see what potentially-vulnerable
technology powers your site*/
app.disable("x-powered-by");
// Parse API queries
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
// Redirect .well-known requests .well-known/change-password
app.get("/.well-known/change-password", (request, response) => {
response.redirect(301, "/change-password");
});
// Proxy API
app.use(
"/api",
proxy("https://api.domain.tld", {
https: true,
proxyReqPathResolver(request) {
const reqPath = url.parse(request.url).path;
return [api, reqPath].join("/");
},
proxyErrorHandler(error, next) {
console.log(error)
next(error);
}
})
);
// Send pre-compressed static files
app.use(
"/",
expressStaticGzip("/public", {
enableBrotli: true,
orderPreference: ["br", "gz"]
})
);
// Routes
app.use(
gatsyExpress("gatsby-express.json", {
publicDir: "/public",
template: "public/404/index.html",
redirectSlashes: true
})
);
// Allow dotfiles
app.use(
express.static(serverSettings.root, {
dotfiles: "allow"
})
);
// Serve 404 page on 404
app.use((req, res, next) => {
const err = new Error("Not Found");
err.status = 404;
next(err);
});
app.use((err, req, res, next) => {
res.status(err.status || 500);
res.sendFile("404/index.html", {
root: "/public"
});
});
// HTTP server
app
.listen(3000, () => {})
.on("error", e => {
console.log(e);
});
And here is my web.config in case someone need it
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<urlCompression doStaticCompression="false" doDynamicCompression="true" />
<httpProtocol>
<customHeaders>
<remove name="X-Powered-By" />
<add name="Access-Control-Allow-Origin" value="*" />
<add name="X-Frame-Options" value="DENY" />
<add name="Cache-Control" value="public, max-age=0, must-revalidate" />
<add name="X-XSS-Protection" value="1; mode=block" />
<add name="X-Content-Type-Options" value="nosniff" />
<add name="Strict-Transport-Security" value="max-age=31536000; includeSubDomains; preload" />
<add name="Referrer-Policy" value="no-referrer" />
<add name="Content-Security-Policy" value="default-src 'self' https://*.google-analytics.com https://*.google.com; img-src * blob: data:; media-src *; object-src *; script-src * 'unsafe-inline' 'unsafe-eval'; style-src * 'unsafe-inline';" />
<add name="X-Permitted-Cross-Domain-Policies" value="none" />
<add name="Feature-Policy" value="accelerometer 'none'; camera 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; payment 'none'; usb 'none'" />
<add name="X-DNS-Prefetch-Control" value="on"/>
</customHeaders>
</httpProtocol>
<staticContent>
<clientCache cacheControlMode="DisableCache" />
<mimeMap fileExtension=".svg" mimeType="image/svg+xml" />
<mimeMap fileExtension=".json" mimeType="application/json" />
<mimeMap fileExtension=".webmanifest" mimeType="application/manifest+json" />
<mimeMap fileExtension=".*" mimeType="text/plain" />
<mimeMap fileExtension=".mp4" mimeType="video/mp4" />
<mimeMap fileExtension=".m4v" mimeType="video/m4v" />
<mimeMap fileExtension=".aac" mimeType="audio/aac" />
<mimeMap fileExtension=".oga" mimeType="audio/ogg" />
<mimeMap fileExtension=".webm" mimeType="video/webm" />
<mimeMap fileExtension=".ogv" mimeType="video/ogv" />
<mimeMap fileExtension=".ogg" mimeType="video/ogg" />
<mimeMap fileExtension=".m4a" mimeType="video/mp4" />
</staticContent>
<modules runAllManagedModulesForAllRequests="false" />
<iisnode watchedFiles="web.config;*.js;"/>
<handlers>
<add name="iisnode" path="serve.js" verb="*" modules="iisnode" />
</handlers>
<security>
<requestFiltering removeServerHeader="true">
<hiddenSegments>
<remove segment="bin" />
</hiddenSegments>
</requestFiltering>
</security>
<rewrite>
<rules>
<clear />
<rule name="NodeInspector" patternSyntax="ECMAScript" stopProcessing="true">
<match url="^serve.js\/debug[\/]?" />
</rule>
<rule name="app" enabled="true" patternSyntax="ECMAScript" stopProcessing="true">
<match url="iisnode.+" negate="true" />
<conditions logicalGrouping="MatchAll" trackAllCaptures="false" />
<action type="Rewrite" url="serve.js" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>
Has anyone found a solid approach for this?
Is creating the file on postBuild a good solution?
Most helpful comment
Ah! Come on Apple, not making it easy on us!
Let's re-open this and find a more robust solution.