Skip to content

Commit 6511920

Browse files
authored
Merge pull request #868 from atmire/google-scholar-fixes
Google scholar fixes
2 parents b4120f2 + 764cfc5 commit 6511920

14 files changed

Lines changed: 211 additions & 16 deletions

docs/Configuration.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,70 @@ Angulartics can be configured to work with a number of other services besides Go
6363
In order to start using one of these services, select it from the [Angulartics Providers page](https://angulartics.github.io/angulartics2/#providers), and follow the instructions on how to configure it.
6464

6565
The Google Analytics script was added in [`main.browser.ts`](https://github.com/DSpace/dspace-angular/blob/ff04760f4af91ac3e7add5e7424a46cb2439e874/src/main.browser.ts#L33) instead of the `<head>` tag in `index.html` to ensure events get sent when the page is shown in a client's browser, and not when it's rendered on the universal server. Likely you'll want to do the same when adding a new service.
66+
67+
## SEO when hosting REST Api and UI on different servers
68+
69+
Indexers such as Google Scholar require that files are hosted on the same domain as the page that links them. In DSpace 7, Bitstreams are served from the REST server. So if you use different servers for the REST api and the UI you'll want to ensure that Bitstream downloads are proxied through the UI server.
70+
71+
In order to achieve this we'll need to do two things:
72+
- **Proxy the Bitstream downloads through the UI server.** You'll need to put a webserver such as httpd or nginx in front of the UI server in order to achieve this. [Below](#apache-http-server-config) you'll find a section explaining how to do it in httpd.
73+
- **Update the URLs for Bitstream downloads to match the UI server.** This can be done using a setting in the UI environment file.
74+
75+
### UI config
76+
If you set the property `rewriteDownloadUrls` to `true` in your `environment.prod.ts` file, the [origin](https://developer.mozilla.org/en-US/docs/Glossary/Origin) of any download URL will be replaced by the origin of the UI. This will also happen for the `citation_pdf_url` `<meta>` tag on Item pages.
77+
78+
The app will determine the UI origin currently in use, so the external UI URL doesn't need to be configured anywhere and rewrites will still work if you host the UI from multiple domains.
79+
80+
### Apache HTTP Server config
81+
82+
#### Basics
83+
In order to be able to host bitstreams from the UI Server you'll need to enable mod_proxy and add the following to the httpd config of your UI server:
84+
85+
```
86+
ProxyPassMatch "/server/api/core/bitstreams/([^/]+)/content" "http://rest.api/server/api/core/bitstreams/$1/content"
87+
ProxyPassReverse "/server/api/core/bitstreams/([^/]+)/content" "http://rest.api/server/api/core/bitstreams/$1/content"
88+
```
89+
90+
Replace http://rest.api in with the correct origin for your REST server.
91+
92+
The `ProxyPassMatch` line forwards all requests matching the regular expression for a bitstream download URL to the corresponding path on the REST server
93+
94+
The `ProxyPassReverse` ensures that if the REST server were to return redirect response, httpd would also swap out its hostname for the hostname of the UI before forwarding the response to the client.
95+
96+
#### Using HTTPS
97+
If your REST server uses https, you'll need to enable mod_ssl and ensure `SSLProxyEngine on` is part of your UI server's httpd config as well
98+
99+
If the UI hostname doesn't match the CN in the SSL certificate of the REST server (which is likely if they're on different domains), you'll also need to add the following lines
100+
101+
```
102+
SSLProxyCheckPeerCN off
103+
SSLProxyCheckPeerName off
104+
```
105+
These are two names for [the same directive](https://httpd.apache.org/docs/trunk/mod/mod_ssl.html#sslproxycheckpeername) that have been used for various versions of httpd, old versions need the former, then some in-between versions need both, and newer versions only need the latter. Keeping them both doesn't harm anything.
106+
107+
So the entire config becomes:
108+
109+
```
110+
SSLProxyEngine on
111+
SSLProxyCheckPeerCN off
112+
SSLProxyCheckPeerName off
113+
ProxyPassMatch "/server/api/core/bitstreams/([^/]+)/content" "https://rest.api/server/api/core/bitstreams/$1/content"
114+
ProxyPassReverse "/server/api/core/bitstreams/([^/]+)/content" "https://rest.api/server/api/core/bitstreams/$1/content"
115+
```
116+
117+
If you don't want httpd to verify the certificate of the REST server, you can also turn all checks off with the following config:
118+
119+
```
120+
SSLProxyEngine on
121+
SSLProxyVerify none
122+
SSLProxyCheckPeerCN off
123+
SSLProxyCheckPeerName off
124+
SSLProxyCheckPeerExpire off
125+
ProxyPassMatch "/server/api/core/bitstreams/([^/]+)/content" "https://rest.api/server/api/core/bitstreams/$1/content"
126+
ProxyPassReverse "/server/api/core/bitstreams/([^/]+)/content" "https://rest.api/server/api/core/bitstreams/$1/content"
127+
```
128+
129+
130+
131+
132+

scripts/set-env.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ function generateEnvironmentFile(file: GlobalConfig): void {
5757

5858
// TODO remove workaround in beta 5
5959
if (file.rest.nameSpace.match("(.*)/api/?$") !== null) {
60-
const newValue = getNameSpace(file.rest.nameSpace);
61-
console.log(colors.white.bgMagenta.bold(`The rest.nameSpace property in your environment file or in your DSPACE_REST_NAMESPACE environment variable ends with '/api'.\nThis is deprecated. As '/api' isn't configurable on the rest side, it shouldn't be repeated in every environment file.\nPlease change the rest nameSpace to '${newValue}'`));
60+
file.rest.nameSpace = getNameSpace(file.rest.nameSpace);
61+
console.log(colors.white.bgMagenta.bold(`The rest.nameSpace property in your environment file or in your DSPACE_REST_NAMESPACE environment variable ends with '/api'.\nThis is deprecated. As '/api' isn't configurable on the rest side, it shouldn't be repeated in every environment file.\nPlease change the rest nameSpace to '${file.rest.nameSpace}'`));
6262
}
6363

6464
const contents = `export const environment = ` + JSON.stringify(file);

src/app/core/metadata/metadata.service.spec.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,13 @@ import { UUIDService } from '../shared/uuid.service';
5252
import { MetadataService } from './metadata.service';
5353
import { environment } from '../../../environments/environment';
5454
import { storeModuleConfig } from '../../app.reducer';
55+
import { HardRedirectService } from '../services/hard-redirect.service';
56+
import { URLCombiner } from '../url-combiner/url-combiner';
5557

5658
/* tslint:disable:max-classes-per-file */
5759
@Component({
58-
template: `<router-outlet></router-outlet>`
60+
template: `
61+
<router-outlet></router-outlet>`
5962
})
6063
class TestComponent {
6164
constructor(private metadata: MetadataService) {
@@ -170,6 +173,7 @@ describe('MetadataService', () => {
170173
Title,
171174
// tslint:disable-next-line:no-empty
172175
{ provide: ItemDataService, useValue: { findById: () => {} } },
176+
{ provide: HardRedirectService, useValue: { rewriteDownloadURL: (a) => a, getRequestOrigin: () => environment.ui.baseUrl }},
173177
BrowseService,
174178
MetadataService
175179
],
@@ -208,7 +212,7 @@ describe('MetadataService', () => {
208212
tick();
209213
expect(tagStore.get('citation_dissertation_name')[0].content).toEqual('Test PowerPoint Document');
210214
expect(tagStore.get('citation_dissertation_institution')[0].content).toEqual('Mock Publisher');
211-
expect(tagStore.get('citation_abstract_html_url')[0].content).toEqual([environment.ui.baseUrl, router.url].join(''));
215+
expect(tagStore.get('citation_abstract_html_url')[0].content).toEqual(new URLCombiner(environment.ui.baseUrl, router.url).toString());
212216
expect(tagStore.get('citation_pdf_url')[0].content).toEqual('https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/content');
213217
}));
214218

src/app/core/metadata/metadata.service.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Inject, Injectable } from '@angular/core';
1+
import { Injectable } from '@angular/core';
22

33
import { Meta, MetaDefinition, Title } from '@angular/platform-browser';
44
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
@@ -20,7 +20,8 @@ import { Bitstream } from '../shared/bitstream.model';
2020
import { DSpaceObject } from '../shared/dspace-object.model';
2121
import { Item } from '../shared/item.model';
2222
import { getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteListPayload } from '../shared/operators';
23-
import { environment } from '../../../environments/environment';
23+
import { HardRedirectService } from '../services/hard-redirect.service';
24+
import { URLCombiner } from '../url-combiner/url-combiner';
2425

2526
@Injectable()
2627
export class MetadataService {
@@ -39,6 +40,7 @@ export class MetadataService {
3940
private dsoNameService: DSONameService,
4041
private bitstreamDataService: BitstreamDataService,
4142
private bitstreamFormatDataService: BitstreamFormatDataService,
43+
private redirectService: HardRedirectService
4244
) {
4345
// TODO: determine what open graph meta tags are needed and whether
4446
// the differ per route. potentially add image based on DSpaceObject
@@ -254,7 +256,7 @@ export class MetadataService {
254256
*/
255257
private setCitationAbstractUrlTag(): void {
256258
if (this.currentObject.value instanceof Item) {
257-
const value = [environment.ui.baseUrl, this.router.url].join('');
259+
const value = new URLCombiner(this.redirectService.getRequestOrigin(), this.router.url).toString();
258260
this.addMetaTag('citation_abstract_html_url', value);
259261
}
260262
}
@@ -279,7 +281,8 @@ export class MetadataService {
279281
getFirstSucceededRemoteDataPayload()
280282
).subscribe((format: BitstreamFormat) => {
281283
if (format.mimetype === 'application/pdf') {
282-
this.addMetaTag('citation_pdf_url', bitstream._links.content.href);
284+
const rewrittenURL= this.redirectService.rewriteDownloadURL(bitstream._links.content.href);
285+
this.addMetaTag('citation_pdf_url', rewrittenURL);
283286
}
284287
});
285288
}

src/app/core/services/browser-hard-redirect.service.spec.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import {TestBed} from '@angular/core/testing';
22
import {BrowserHardRedirectService} from './browser-hard-redirect.service';
33

44
describe('BrowserHardRedirectService', () => {
5-
5+
const origin = 'test origin';
66
const mockLocation = {
77
href: undefined,
88
pathname: '/pathname',
99
search: '/search',
10+
origin
1011
} as Location;
1112

1213
const service: BrowserHardRedirectService = new BrowserHardRedirectService(mockLocation);
@@ -38,4 +39,12 @@ describe('BrowserHardRedirectService', () => {
3839
expect(service.getCurrentRoute()).toEqual(mockLocation.pathname + mockLocation.search);
3940
});
4041
});
42+
43+
describe('when requesting the origin', () => {
44+
45+
it('should return the location origin', () => {
46+
expect(service.getRequestOrigin()).toEqual(origin);
47+
});
48+
});
49+
4150
});

src/app/core/services/browser-hard-redirect.service.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ export function locationProvider(): Location {
1111
* Service for performing hard redirects within the browser app module
1212
*/
1313
@Injectable()
14-
export class BrowserHardRedirectService implements HardRedirectService {
14+
export class BrowserHardRedirectService extends HardRedirectService {
1515

1616
constructor(
1717
@Inject(LocationToken) protected location: Location,
1818
) {
19+
super();
1920
}
2021

2122
/**
@@ -32,4 +33,11 @@ export class BrowserHardRedirectService implements HardRedirectService {
3233
getCurrentRoute() {
3334
return this.location.pathname + this.location.search;
3435
}
36+
37+
/**
38+
* Get the hostname of the request
39+
*/
40+
getRequestOrigin() {
41+
return this.location.origin;
42+
}
3543
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { HardRedirectService } from './hard-redirect.service';
2+
import { environment } from '../../../environments/environment';
3+
import { TestBed } from '@angular/core/testing';
4+
import { Injectable } from '@angular/core';
5+
6+
const requestOrigin = 'http://dspace-angular-ui.dspace.com';
7+
8+
describe('HardRedirectService', () => {
9+
let service: TestHardRedirectService;
10+
11+
beforeEach(() => {
12+
TestBed.configureTestingModule({ providers: [TestHardRedirectService] });
13+
service = TestBed.get(TestHardRedirectService);
14+
});
15+
16+
describe('when calling rewriteDownloadURL', () => {
17+
let originalValue;
18+
const relativePath = '/test/url/path';
19+
const testURL = environment.rest.baseUrl + relativePath;
20+
beforeEach(() => {
21+
originalValue = environment.rewriteDownloadUrls;
22+
});
23+
24+
it('it should return the same url when rewriteDownloadURL is false', () => {
25+
environment.rewriteDownloadUrls = false;
26+
expect(service.rewriteDownloadURL(testURL)).toEqual(testURL);
27+
});
28+
29+
it('it should replace part of the url when rewriteDownloadURL is true', () => {
30+
environment.rewriteDownloadUrls = true;
31+
expect(service.rewriteDownloadURL(testURL)).toEqual(requestOrigin + environment.rest.nameSpace + relativePath);
32+
});
33+
34+
afterEach(() => {
35+
environment.rewriteDownloadUrls = originalValue;
36+
})
37+
});
38+
});
39+
40+
@Injectable()
41+
class TestHardRedirectService extends HardRedirectService {
42+
constructor() {
43+
super();
44+
}
45+
46+
redirect(url: string) {
47+
return undefined;
48+
}
49+
50+
getCurrentRoute() {
51+
return undefined;
52+
}
53+
54+
getRequestOrigin() {
55+
return requestOrigin;
56+
}
57+
}

src/app/core/services/hard-redirect.service.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { Injectable } from '@angular/core';
2+
import { environment } from '../../../environments/environment';
3+
import { URLCombiner } from '../url-combiner/url-combiner';
24

35
/**
46
* Service to take care of hard redirects
@@ -19,4 +21,20 @@ export abstract class HardRedirectService {
1921
* e.g. /search?page=1&query=open%20access&f.dateIssued.min=1980&f.dateIssued.max=2020
2022
*/
2123
abstract getCurrentRoute();
24+
25+
/**
26+
* Get the hostname of the request
27+
*/
28+
abstract getRequestOrigin();
29+
30+
public rewriteDownloadURL(originalUrl: string): string {
31+
if (environment.rewriteDownloadUrls) {
32+
const hostName = this.getRequestOrigin();
33+
const namespace = environment.rest.nameSpace;
34+
const rewrittenUrl = new URLCombiner(hostName, namespace).toString();
35+
return originalUrl.replace(environment.rest.baseUrl, rewrittenUrl);
36+
} else {
37+
return originalUrl;
38+
}
39+
}
2240
}

src/app/core/services/server-hard-redirect.service.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@ describe('ServerHardRedirectService', () => {
77
const mockResponse = jasmine.createSpyObj(['redirect', 'end']);
88

99
const service: ServerHardRedirectService = new ServerHardRedirectService(mockRequest, mockResponse);
10+
const origin = 'test-host';
1011

1112
beforeEach(() => {
13+
mockRequest.headers = {
14+
host: 'test-host',
15+
};
16+
1217
TestBed.configureTestingModule({});
1318
});
1419

@@ -40,4 +45,12 @@ describe('ServerHardRedirectService', () => {
4045
expect(service.getCurrentRoute()).toEqual(mockRequest.originalUrl);
4146
});
4247
});
48+
49+
describe('when requesting the origin', () => {
50+
51+
it('should return the location origin', () => {
52+
expect(service.getRequestOrigin()).toEqual(origin);
53+
});
54+
});
55+
4356
});

src/app/core/services/server-hard-redirect.service.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ import { HardRedirectService } from './hard-redirect.service';
77
* Service for performing hard redirects within the server app module
88
*/
99
@Injectable()
10-
export class ServerHardRedirectService implements HardRedirectService {
10+
export class ServerHardRedirectService extends HardRedirectService {
1111

1212
constructor(
1313
@Inject(REQUEST) protected req: Request,
1414
@Inject(RESPONSE) protected res: Response,
1515
) {
16+
super();
1617
}
1718

1819
/**
@@ -59,4 +60,11 @@ export class ServerHardRedirectService implements HardRedirectService {
5960
getCurrentRoute() {
6061
return this.req.originalUrl;
6162
}
63+
64+
/**
65+
* Get the hostname of the request
66+
*/
67+
getRequestOrigin() {
68+
return this.req.headers.host;
69+
}
6270
}

0 commit comments

Comments
 (0)